Compare commits
1647 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
177d7ed07a | ||
|
|
85a1e15b58 | ||
|
|
432b58a078 | ||
|
|
75e3c5daef | ||
|
|
deb71c27b0 | ||
|
|
8f5e1de6d8 | ||
|
|
4836d62d7a | ||
|
|
d27b0617b2 | ||
|
|
28a2c29a39 | ||
|
|
fcb6478407 | ||
|
|
6d72afe40e | ||
|
|
e775266c64 | ||
|
|
12f25b38c0 | ||
|
|
c67a1107cb | ||
|
|
34bfb0d62c | ||
|
|
ede45cad1f | ||
|
|
75fe4c8aed | ||
|
|
12e272a7e5 | ||
|
|
cfcba4e578 | ||
|
|
37ab898426 | ||
|
|
68865ea929 | ||
|
|
86674faa22 | ||
|
|
f07947ce45 | ||
|
|
6a50f59e25 | ||
|
|
bfacd56800 | ||
|
|
45dea8b0c1 | ||
|
|
2f7f8cf2d8 | ||
|
|
31611b6a28 | ||
|
|
d1cd4b0c2b | ||
|
|
c8ba1c3e7c | ||
|
|
fbc8fe4c2d | ||
|
|
54ec9b48db | ||
|
|
488698d5e2 | ||
|
|
58c407aabb | ||
|
|
fe750f23bc | ||
|
|
87a01a5cfd | ||
|
|
74dd669bb0 | ||
|
|
36a50389f5 | ||
|
|
4f2d7434c7 | ||
|
|
b0a0848476 | ||
|
|
9fcd897e54 | ||
|
|
daa8fff21e | ||
|
|
785229ddea | ||
|
|
8bb11bf1d4 | ||
|
|
1f975e15c1 | ||
|
|
6c69ba54db | ||
|
|
49f9904d00 | ||
|
|
7afd0dfa4e | ||
|
|
b1b6a437a7 | ||
|
|
e4d5006591 | ||
|
|
627b3771d3 | ||
|
|
f4758e84e8 | ||
|
|
8dfe2098ed | ||
|
|
c56a4ee036 | ||
|
|
c32623b821 | ||
|
|
3cd0a947f7 | ||
|
|
8eea1cf4e7 | ||
|
|
b5fccd5bbe | ||
|
|
e74ce9dfd8 | ||
|
|
3743365a83 | ||
|
|
8aeb2173d1 | ||
|
|
9a2b17d952 | ||
|
|
6f54cce01a | ||
|
|
6901b2049e | ||
|
|
d0dcc027df | ||
|
|
b693005118 | ||
|
|
ab4a0e836f | ||
|
|
abe02db6c6 | ||
|
|
a2cd5dd32d | ||
|
|
49b46a6096 | ||
|
|
94f420ca3f | ||
|
|
5e530105df | ||
|
|
2f82d34c4b | ||
|
|
81fd01d0ac | ||
|
|
9faac9f9fe | ||
|
|
d04787a60c | ||
|
|
f5dbf94b52 | ||
|
|
5bec2d9b15 | ||
|
|
fe64f0c63c | ||
|
|
c20fd9691a | ||
|
|
eb323fbff9 | ||
|
|
211f6b9a74 | ||
|
|
b6c003ec63 | ||
|
|
93d4bf2a72 | ||
|
|
c6cb573383 | ||
|
|
f4ce671ea4 | ||
|
|
147f7cbabb | ||
|
|
b05d5a141e | ||
|
|
d34e0306f8 | ||
|
|
bd9f48dd24 | ||
|
|
9805990d79 | ||
|
|
dbbe60967c | ||
|
|
0ef91c1904 | ||
|
|
376573459c | ||
|
|
9c6d7c0ff9 | ||
|
|
30a95b7da3 | ||
|
|
e6a60aef9a | ||
|
|
5c2024581f | ||
|
|
f7ea2bb51e | ||
|
|
3e4da8ab57 | ||
|
|
7352a28908 | ||
|
|
d1928ee578 | ||
|
|
cd978d7384 | ||
|
|
cde0d8f5e2 | ||
|
|
7bacfcc2e4 | ||
|
|
241fe36103 | ||
|
|
441714a656 | ||
|
|
bd3fdb7f16 | ||
|
|
775af6feee | ||
|
|
adf5c17e0d | ||
|
|
beb2d96a32 | ||
|
|
2a4ae88bc0 | ||
|
|
b76098ba45 | ||
|
|
c095027f8e | ||
|
|
9d1db19907 | ||
|
|
260e321537 | ||
|
|
073603b527 | ||
|
|
17b259cf31 | ||
|
|
8f0f0026e9 | ||
|
|
59dae2b545 | ||
|
|
4670f69ead | ||
|
|
16fbd25a34 | ||
|
|
2d75985cb3 | ||
|
|
f963fb321e | ||
|
|
10feea0d48 | ||
|
|
b6b9b0ac36 | ||
|
|
5551e85853 | ||
|
|
1f0fa5031b | ||
|
|
263294a3f5 | ||
|
|
f9df30f70b | ||
|
|
61d31ec054 | ||
|
|
c8917bfc4c | ||
|
|
36b69a05e5 | ||
|
|
c8d2f66467 | ||
|
|
7416bb0e56 | ||
|
|
9182d0132d | ||
|
|
9be9357ade | ||
|
|
7f414f8adf | ||
|
|
3af9549939 | ||
|
|
41f248d731 | ||
|
|
18a517b7bf | ||
|
|
5150204389 | ||
|
|
3b16e7729d | ||
|
|
76d27c9fce | ||
|
|
4ce6e41000 | ||
|
|
33260cdbd9 | ||
|
|
85d81ba7fd | ||
|
|
7e8a3ca21f | ||
|
|
e9e4dc1f5c | ||
|
|
17c30e165a | ||
|
|
c45c6ceb15 | ||
|
|
d73c2c465f | ||
|
|
d4fc53939b | ||
|
|
4becb65bec | ||
|
|
f64e16c790 | ||
|
|
1772011627 | ||
|
|
5b8f785e2b | ||
|
|
a0e3b77006 | ||
|
|
908070ecd7 | ||
|
|
7c6a58cd30 | ||
|
|
b0990a1132 | ||
|
|
c6988cdb88 | ||
|
|
e0d304b033 | ||
|
|
e4a9f2d64c | ||
|
|
0236fe3ca9 | ||
|
|
1bed8623a2 | ||
|
|
df7d957914 | ||
|
|
30c4b00f33 | ||
|
|
ab27886460 | ||
|
|
31e18d04d7 | ||
|
|
8155484510 | ||
|
|
b61f1d2b53 | ||
|
|
2e274b936a | ||
|
|
bf3e311b57 | ||
|
|
6a7613de6b | ||
|
|
ee46549e04 | ||
|
|
377f3d4aff | ||
|
|
752d47d71e | ||
|
|
367157b80c | ||
|
|
53542f1cd6 | ||
|
|
7a8f156abf | ||
|
|
c60cc57a0d | ||
|
|
8de6ec9a21 | ||
|
|
44b6f4be7e | ||
|
|
280be1751c | ||
|
|
701a73a2c5 | ||
|
|
b578eada07 | ||
|
|
8100f155dc | ||
|
|
9f1a014004 | ||
|
|
e35e0e157c | ||
|
|
3aff328af3 | ||
|
|
ffb086045a | ||
|
|
c0786dfa6f | ||
|
|
ddc33fa52b | ||
|
|
9f2d6a5d41 | ||
|
|
64e884a092 | ||
|
|
17ec174683 | ||
|
|
3666cbee94 | ||
|
|
25de018f7d | ||
|
|
6597851b48 | ||
|
|
0399131968 | ||
|
|
86836e7f89 | ||
|
|
df346b11d3 | ||
|
|
27f74b3fe2 | ||
|
|
87dec64ad1 | ||
|
|
54c787162a | ||
|
|
6e92e699dc | ||
|
|
7950f43db3 | ||
|
|
d300677315 | ||
|
|
bd4d29dd14 | ||
|
|
18a84433f4 | ||
|
|
768ebf0ef2 | ||
|
|
1c20cb5478 | ||
|
|
279587ea11 | ||
|
|
25df193390 | ||
|
|
9ce81693bd | ||
|
|
16765e092f | ||
|
|
237e1257c4 | ||
|
|
665859b17d | ||
|
|
b5a6d6974c | ||
|
|
26bab029f4 | ||
|
|
ed7bb07b03 | ||
|
|
c87277ad01 | ||
|
|
62be259a90 | ||
|
|
80798f984b | ||
|
|
e32409880c | ||
|
|
7b6eb2940e | ||
|
|
87ad8df22f | ||
|
|
9fe20036a1 | ||
|
|
2b0d8d43bd | ||
|
|
3af340d384 | ||
|
|
20c1ad8d87 | ||
|
|
8c351c7c46 | ||
|
|
6647e986d9 | ||
|
|
77f691520c | ||
|
|
dfaa6ec024 | ||
|
|
41c574f5df | ||
|
|
10e901dcaa | ||
|
|
1d3626b4e1 | ||
|
|
bc419a51d6 | ||
|
|
d0980a2872 | ||
|
|
74719e48d9 | ||
|
|
d7c6b45438 | ||
|
|
551cfd87ee | ||
|
|
ba29873a8e | ||
|
|
d9e2bb4537 | ||
|
|
16d7b15d67 | ||
|
|
b9da97fedd | ||
|
|
bdf3b0393a | ||
|
|
a18f701466 | ||
|
|
71c7d8a90c | ||
|
|
99e766d952 | ||
|
|
fc78a0ed36 | ||
|
|
d4d398f583 | ||
|
|
be766ec803 | ||
|
|
57bb8dbbe3 | ||
|
|
c539d4fbbd | ||
|
|
a7dddcebe8 | ||
|
|
a9275845ff | ||
|
|
e5bf9efdb9 | ||
|
|
85073345ec | ||
|
|
80d5b29902 | ||
|
|
f55b748d20 | ||
|
|
870468ddf7 | ||
|
|
b3107916ce | ||
|
|
9cf856ab78 | ||
|
|
bbd047a940 | ||
|
|
d3dfb0a7ff | ||
|
|
5a8f9db79c | ||
|
|
fbe20386b6 | ||
|
|
6245b40015 | ||
|
|
3e959d8dc0 | ||
|
|
564df920d1 | ||
|
|
7e2c467a4f | ||
|
|
3a42305408 | ||
|
|
09616777e6 | ||
|
|
c2a5569b8c | ||
|
|
38bea2108b | ||
|
|
ab0777b45f | ||
|
|
962f94387b | ||
|
|
de5600b4fd | ||
|
|
188a202f02 | ||
|
|
19f39b87f5 | ||
|
|
c759f314f9 | ||
|
|
6c98f14c64 | ||
|
|
f2348e1b24 | ||
|
|
4b5a10fe61 | ||
|
|
163fa58b5a | ||
|
|
2bb03225cb | ||
|
|
b3bbb6af01 | ||
|
|
28d711e1f4 | ||
|
|
410fdb8343 | ||
|
|
21c2f3bdd1 | ||
|
|
baae080318 | ||
|
|
81c0796056 | ||
|
|
6ea5c5f414 | ||
|
|
7e37705843 | ||
|
|
425b5e6b4a | ||
|
|
8942c72fb2 | ||
|
|
e9359fdd73 | ||
|
|
5c4308abc1 | ||
|
|
61576b671b | ||
|
|
1e4d6bb942 | ||
|
|
ff6c0addb4 | ||
|
|
98cd524c07 | ||
|
|
746d57ff42 | ||
|
|
7319822419 | ||
|
|
58bcde3818 | ||
|
|
b57d08f38e | ||
|
|
739a8cef32 | ||
|
|
8a6e31e025 | ||
|
|
616aac9771 | ||
|
|
23a1b7484e | ||
|
|
9624ee1c76 | ||
|
|
4c557be2c2 | ||
|
|
7577ee8015 | ||
|
|
8c73914da4 | ||
|
|
604ba300aa | ||
|
|
876166ab74 | ||
|
|
1bf8fda770 | ||
|
|
cd32218cea | ||
|
|
0fd0974738 | ||
|
|
ed7f95a1a7 | ||
|
|
a36c1c52ae | ||
|
|
226d948c4d | ||
|
|
84c67f977e | ||
|
|
c09cda84a3 | ||
|
|
bd36ea1829 | ||
|
|
01a47925e0 | ||
|
|
dd8a70eb95 | ||
|
|
013bdba4ff | ||
|
|
c1acc54d55 | ||
|
|
5f3fb6e5f7 | ||
|
|
e3fac9c161 | ||
|
|
accab9e78a | ||
|
|
cb53d140e3 | ||
|
|
72986d1946 | ||
|
|
2195c55518 | ||
|
|
c418ba1908 | ||
|
|
934356e5cc | ||
|
|
c40235a910 | ||
|
|
cd8338196e | ||
|
|
2143e87401 | ||
|
|
f0a536ee1e | ||
|
|
dde4b63c6b | ||
|
|
0e7bcc4b56 | ||
|
|
e4816b4322 | ||
|
|
af4f29c538 | ||
|
|
016adb64ef | ||
|
|
7cedccedcd | ||
|
|
a9c12d4534 | ||
|
|
77f71b1978 | ||
|
|
1c029fbc7b | ||
|
|
2bc0d8d145 | ||
|
|
4c2af88f92 | ||
|
|
ddd5d2a0b0 | ||
|
|
d5cb59dc84 | ||
|
|
f21581630a | ||
|
|
3fef37d06b | ||
|
|
a8b93896ed | ||
|
|
6c1e9bf0ca | ||
|
|
1d8947d404 | ||
|
|
834377b342 | ||
|
|
c2e0eb05e5 | ||
|
|
256032ca4a | ||
|
|
d80f2b9566 | ||
|
|
9e2f0371ba | ||
|
|
a2e74a3e1b | ||
|
|
f04a5a1ab9 | ||
|
|
7114311b75 | ||
|
|
1e9e092dc3 | ||
|
|
98b3a5ba23 | ||
|
|
200966e806 | ||
|
|
407430b81e | ||
|
|
c4b1fc039c | ||
|
|
d00379af6b | ||
|
|
7cee0f3ee3 | ||
|
|
2588346f1b | ||
|
|
863128013d | ||
|
|
51eb8eb67f | ||
|
|
28e01fd8ac | ||
|
|
726e153ad5 | ||
|
|
e8df09c85b | ||
|
|
2b4fb2971d | ||
|
|
933b36cca0 | ||
|
|
37a7cfb6ba | ||
|
|
8a1cda159c | ||
|
|
403dcbebcd | ||
|
|
373ad69306 | ||
|
|
b2029e09f5 | ||
|
|
4f37d2d0c2 | ||
|
|
a5b07bc2a8 | ||
|
|
1544a5622d | ||
|
|
d49a877771 | ||
|
|
0f4747743c | ||
|
|
368e4522e7 | ||
|
|
cb1d1bb115 | ||
|
|
612cc3dd41 | ||
|
|
bff082e577 | ||
|
|
ea41750a14 | ||
|
|
458376a665 | ||
|
|
f0284907c4 | ||
|
|
ad0385ccf7 | ||
|
|
4350fc3c65 | ||
|
|
fc3422b9e5 | ||
|
|
9703c19fb4 | ||
|
|
d36f2fb354 | ||
|
|
c837785314 | ||
|
|
33014a9f45 | ||
|
|
c16d0b8605 | ||
|
|
c88e4c5173 | ||
|
|
0540696c3e | ||
|
|
5f59a97a02 | ||
|
|
d3b815c3c7 | ||
|
|
d2a8dcbede | ||
|
|
4854e879a6 | ||
|
|
ddc99cebff | ||
|
|
47470d4f2b | ||
|
|
d9297d54de | ||
|
|
2d821f957e | ||
|
|
c0ce6e7a8a | ||
|
|
3f620c6cdd | ||
|
|
9160d8018d | ||
|
|
51307bcc69 | ||
|
|
d29651ee80 | ||
|
|
18640077aa | ||
|
|
59563c893b | ||
|
|
90fd1786e1 | ||
|
|
4a11115dd0 | ||
|
|
baf3e774c5 | ||
|
|
ac296411d5 | ||
|
|
09cea4d6d4 | ||
|
|
382ff2416f | ||
|
|
27036379dd | ||
|
|
f4e6e140e0 | ||
|
|
b4e5cb88d9 | ||
|
|
3269fef845 | ||
|
|
e797719b41 | ||
|
|
284a8f2465 | ||
|
|
54ea656da2 | ||
|
|
b4aaadf40b | ||
|
|
74d2e3ef75 | ||
|
|
b10e4c11d9 | ||
|
|
075e141a9c | ||
|
|
04acf895f0 | ||
|
|
21608bf2e2 | ||
|
|
653beb1952 | ||
|
|
050d33ff14 | ||
|
|
1ae86e039b | ||
|
|
f75cadf6ba | ||
|
|
9acb980b82 | ||
|
|
93d0fe9176 | ||
|
|
6a15cd0566 | ||
|
|
614fe3f703 | ||
|
|
82d4bb3046 | ||
|
|
f49c13b1b3 | ||
|
|
828b817aca | ||
|
|
7f26f01743 | ||
|
|
50da63fc63 | ||
|
|
f8f6508449 | ||
|
|
7256eb0935 | ||
|
|
cb08b42e54 | ||
|
|
9e7caeff94 | ||
|
|
e72a1d73be | ||
|
|
aaacad81e7 | ||
|
|
55ee5b3b01 | ||
|
|
94bf2748be | ||
|
|
9a4aa7c1fa | ||
|
|
3e71365a95 | ||
|
|
018ffcea7c | ||
|
|
e24ba9f495 | ||
|
|
0e2e069503 | ||
|
|
c4bbff3802 | ||
|
|
290054ef5d | ||
|
|
4c25828540 | ||
|
|
5eda4888ed | ||
|
|
7c322d9411 | ||
|
|
bd35d4e78a | ||
|
|
7d623faf4b | ||
|
|
6eb711e70b | ||
|
|
81ff0152c0 | ||
|
|
8a07f9f57e | ||
|
|
ca367d0fe7 | ||
|
|
cd7adbd792 | ||
|
|
5b76ec9f68 | ||
|
|
bb21270aab | ||
|
|
22f3a54fbf | ||
|
|
6644711969 | ||
|
|
989df4a8a4 | ||
|
|
d5423c88ea | ||
|
|
5838b7a455 | ||
|
|
86e8cedfab | ||
|
|
93c31c5433 | ||
|
|
4ca8fddd50 | ||
|
|
8cc46fd2a3 | ||
|
|
b2d8f53a5c | ||
|
|
8e8e59addc | ||
|
|
63e52878a1 | ||
|
|
ef276bd51e | ||
|
|
7ac3784f32 | ||
|
|
e12133e24b | ||
|
|
be01781373 | ||
|
|
65523768f9 | ||
|
|
f602ea88e2 | ||
|
|
0f2401b0cc | ||
|
|
da6870cf1e | ||
|
|
06e420aa70 | ||
|
|
c667e64d7f | ||
|
|
5c3dd8b09d | ||
|
|
f7c528277b | ||
|
|
2ff33b5010 | ||
|
|
d2f4e3ee74 | ||
|
|
730486b27b | ||
|
|
c4b4a93a0d | ||
|
|
f34213a147 | ||
|
|
862f8b4ce6 | ||
|
|
5a2afa9b89 | ||
|
|
4759d178d3 | ||
|
|
777fb78abc | ||
|
|
faa24caf5b | ||
|
|
5e6529363b | ||
|
|
a785ebef65 | ||
|
|
4137de5adf | ||
|
|
f012e6092f | ||
|
|
9ce4929d87 | ||
|
|
8efe844474 | ||
|
|
02cb80daa1 | ||
|
|
e9d1951d48 | ||
|
|
a11cc28dc7 | ||
|
|
a8fdd6726e | ||
|
|
7ad8a99373 | ||
|
|
c0a24108ba | ||
|
|
ae9b8a0380 | ||
|
|
31a0b01a27 | ||
|
|
efcb73e0d1 | ||
|
|
f008c74419 | ||
|
|
4a646d4700 | ||
|
|
657b262d92 | ||
|
|
988412fc07 | ||
|
|
70750d2c43 | ||
|
|
9c1db98f67 | ||
|
|
12c44fda6f | ||
|
|
a42756ba24 | ||
|
|
6ccdb6cefd | ||
|
|
9f6ce64a31 | ||
|
|
3e35604df0 | ||
|
|
01a69ef15c | ||
|
|
5adbc98c2b | ||
|
|
fb045f1836 | ||
|
|
ee49b8b2a2 | ||
|
|
5ba72b4894 | ||
|
|
6975db6ecf | ||
|
|
8134aca14d | ||
|
|
215bbbd29c | ||
|
|
c4b6f65404 | ||
|
|
69f40e6f6a | ||
|
|
20725900b6 | ||
|
|
ab984729f5 | ||
|
|
8553326c1b | ||
|
|
9f8551058d | ||
|
|
5895871fad | ||
|
|
c372d69e98 | ||
|
|
bacaee138a | ||
|
|
3add61ec57 | ||
|
|
02ae50eef0 | ||
|
|
b308259e6f | ||
|
|
14a0afc7c0 | ||
|
|
d74daf39c7 | ||
|
|
eb091caf4a | ||
|
|
424cc6d93b | ||
|
|
3bacfecc49 | ||
|
|
64dd033c94 | ||
|
|
caec04f63b | ||
|
|
2e79781711 | ||
|
|
289ffbbedc | ||
|
|
e0f0751b28 | ||
|
|
f30be00eb9 | ||
|
|
ee94b8a587 | ||
|
|
fd3f928d81 | ||
|
|
ba08745c23 | ||
|
|
573932efba | ||
|
|
31344a1c75 | ||
|
|
c7f37047b0 | ||
|
|
2a06405a58 | ||
|
|
9206b1a1b5 | ||
|
|
5989ab344d | ||
|
|
a1e024e228 | ||
|
|
8628d3b671 | ||
|
|
7ce4104d2f | ||
|
|
a26a5d6f5a | ||
|
|
e98a5aeff0 | ||
|
|
4c375780c7 | ||
|
|
48415fb1f3 | ||
|
|
8da7cf73c1 | ||
|
|
ba30f460ee | ||
|
|
e1aa0bc7ae | ||
|
|
3987e83b7a | ||
|
|
858d976637 | ||
|
|
48d2978997 | ||
|
|
1872fbd021 | ||
|
|
bbb1b4a7b9 | ||
|
|
aa0ddaae95 | ||
|
|
cb3706e5cf | ||
|
|
3d8f6576aa | ||
|
|
24f7bc6596 | ||
|
|
a1934c87d5 | ||
|
|
7a6b1ec871 | ||
|
|
cfdac1bcb0 | ||
|
|
42ef40282b | ||
|
|
25da6e779c | ||
|
|
7f7204df6c | ||
|
|
3c6d930434 | ||
|
|
b5540fc63a | ||
|
|
f6f58a71b3 | ||
|
|
3a7361a009 | ||
|
|
420f4aacc9 | ||
|
|
163c10b66e | ||
|
|
8fb35571fe | ||
|
|
91f39abc69 | ||
|
|
df63fbbd04 | ||
|
|
5b96576227 | ||
|
|
9f18e2d27d | ||
|
|
1ac6ead4b2 | ||
|
|
9d82e8c27c | ||
|
|
224eb392ba | ||
|
|
8f51418d84 | ||
|
|
c12e5f7a27 | ||
|
|
1c2354dc49 | ||
|
|
2e26e2e126 | ||
|
|
94639f7e0c | ||
|
|
f3957b4fce | ||
|
|
61765d20e1 | ||
|
|
503636f9fb | ||
|
|
4abadec9c4 | ||
|
|
b574489950 | ||
|
|
85788f42a6 | ||
|
|
5fb7f07498 | ||
|
|
fd44b62f26 | ||
|
|
3300c095ed | ||
|
|
55cf0a8447 | ||
|
|
64a87690ee | ||
|
|
3890014250 | ||
|
|
7c2924ae14 | ||
|
|
bfdaf67a9b | ||
|
|
65612f0109 | ||
|
|
e0ade85565 | ||
|
|
c5afc0dc94 | ||
|
|
a7e00c5856 | ||
|
|
2482accb42 | ||
|
|
3e4f71d873 | ||
|
|
fa19f90a6a | ||
|
|
bbadd46766 | ||
|
|
b1f618a98e | ||
|
|
cdf3fe3a25 | ||
|
|
1440841ac8 | ||
|
|
fc57fd2638 | ||
|
|
776cb8d47a | ||
|
|
c36f52415e | ||
|
|
2ee2c5bb55 | ||
|
|
dccb557cd7 | ||
|
|
4570d17ce1 | ||
|
|
b3b3abcdb8 | ||
|
|
6639664b3f | ||
|
|
b99db7cb69 | ||
|
|
3e94e3288f | ||
|
|
1115f9fba2 | ||
|
|
24dde1e4d0 | ||
|
|
7d4caf6974 | ||
|
|
e4ba68850c | ||
|
|
e2154f6561 | ||
|
|
778860c81f | ||
|
|
ee3c56efba | ||
|
|
5dc328724a | ||
|
|
c77ea49594 | ||
|
|
73aa159b98 | ||
|
|
b7d7cffb67 | ||
|
|
eba8db292c | ||
|
|
d5391ef15b | ||
|
|
685cbb1eec | ||
|
|
5842a239fd | ||
|
|
285363aa40 | ||
|
|
92e130d8de | ||
|
|
f674f90eba | ||
|
|
0542b65cbb | ||
|
|
60fa94781e | ||
|
|
38d57533c2 | ||
|
|
e3d6da06a7 | ||
|
|
3af05bb734 | ||
|
|
ffd58cc7ca | ||
|
|
e103c52d27 | ||
|
|
46a4defab7 | ||
|
|
00a71af95d | ||
|
|
2fb8036740 | ||
|
|
6902a8c9b3 | ||
|
|
fd8be0352f | ||
|
|
c19a345f6c | ||
|
|
bdc3ecff5a | ||
|
|
6c4ec29e18 | ||
|
|
412712e62e | ||
|
|
0edcb30f75 | ||
|
|
c0f49b3acb | ||
|
|
b926244085 | ||
|
|
d21136b475 | ||
|
|
8788aaaf25 | ||
|
|
120e800089 | ||
|
|
92dc4b148c | ||
|
|
755dfe6822 | ||
|
|
b787ee1033 | ||
|
|
9ee3612da0 | ||
|
|
fe0b0a9c50 | ||
|
|
dea19f20dd | ||
|
|
f9f70cc6e7 | ||
|
|
23ef1157cb | ||
|
|
8ca4d537ce | ||
|
|
98d5731555 | ||
|
|
5da8929a5d | ||
|
|
e198bafac1 | ||
|
|
dd731399dc | ||
|
|
66fd899ffb | ||
|
|
bca8a33417 | ||
|
|
a11c8d882e | ||
|
|
c5bed48d61 | ||
|
|
98b7d12796 | ||
|
|
93dd8a2213 | ||
|
|
4e4a223f24 | ||
|
|
4a73f3874d | ||
|
|
bc845b2e8d | ||
|
|
83eceb349c | ||
|
|
bb518f0744 | ||
|
|
08ad961123 | ||
|
|
146d494cae | ||
|
|
bb40ecf7c2 | ||
|
|
7de9f64f2a | ||
|
|
0bb6178d49 | ||
|
|
084b3e94a6 | ||
|
|
a0445b5cdd | ||
|
|
1d4ddd373b | ||
|
|
92795963ae | ||
|
|
e634ad6f7d | ||
|
|
ecbae52abe | ||
|
|
e1c5af8602 | ||
|
|
6704a94f36 | ||
|
|
a35403cd91 | ||
|
|
41ead9664b | ||
|
|
e04a9a2579 | ||
|
|
d70af7c9c1 | ||
|
|
d910f5ef3e | ||
|
|
57cba3d511 | ||
|
|
7902b276ad | ||
|
|
f932862ce4 | ||
|
|
ab55b083b4 | ||
|
|
263b3e3682 | ||
|
|
5baad96924 | ||
|
|
68b19c65fe | ||
|
|
3a81302699 | ||
|
|
6b942ecf0b | ||
|
|
1ed0c73525 | ||
|
|
7175b2b05e | ||
|
|
61137610c7 | ||
|
|
b53031972e | ||
|
|
da602eeda0 | ||
|
|
a26025b259 | ||
|
|
25a61c8479 | ||
|
|
e73b64bfed | ||
|
|
b5b8085444 | ||
|
|
388f08a277 | ||
|
|
aa36236ed2 | ||
|
|
a9ca453b17 | ||
|
|
9ab4eb5801 | ||
|
|
26149e7755 | ||
|
|
2d4fd62acf | ||
|
|
e037c8c1b2 | ||
|
|
09687b3811 | ||
|
|
28400f4544 | ||
|
|
7a87b8ebef | ||
|
|
9ff661480b | ||
|
|
ca564bfaad | ||
|
|
dd36877a20 | ||
|
|
1d860fd202 | ||
|
|
e9111ec3bb | ||
|
|
0981ccd0c4 | ||
|
|
077c4ab907 | ||
|
|
efacafaa0d | ||
|
|
1c250bf243 | ||
|
|
c974b91c6b | ||
|
|
6aebe26cc9 | ||
|
|
f93794717e | ||
|
|
0ebf482936 | ||
|
|
b5b8083acd | ||
|
|
ab6bae6a7f | ||
|
|
219658761f | ||
|
|
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 | ||
|
|
6d55544d70 | ||
|
|
d8e38e7e81 | ||
|
|
3dbcf5f293 | ||
|
|
7d230cc15d | ||
|
|
2320b0b852 | ||
|
|
837f99f31c | ||
|
|
663703abae | ||
|
|
5844c031c9 | ||
|
|
c9e95ccc9d | ||
|
|
98b14de02e | ||
|
|
4bc8c57729 | ||
|
|
479861e612 | ||
|
|
a746445b88 | ||
|
|
2c4cb6d42e | ||
|
|
b2cd421e2e | ||
|
|
8a81828a3d | ||
|
|
8e568d0f20 | ||
|
|
3b9759d5e4 | ||
|
|
a20a1f692b | ||
|
|
628b1921b4 | ||
|
|
fd8131201d | ||
|
|
ee39450864 | ||
|
|
34e866007e | ||
|
|
277c0ee818 | ||
|
|
a3c7b3fc35 | ||
|
|
034b492788 | ||
|
|
cd00680c80 | ||
|
|
cc0ebf70a7 | ||
|
|
8087f838ef | ||
|
|
3fa4f8573e | ||
|
|
3eda1750cc | ||
|
|
2c35e27095 | ||
|
|
90487819bf | ||
|
|
50a943c131 | ||
|
|
36b91180e4 | ||
|
|
df44a84bfc | ||
|
|
e5afdb1e04 | ||
|
|
cf2774c852 | ||
|
|
36369068e1 | ||
|
|
af0812e990 | ||
|
|
0285f015e2 | ||
|
|
473b20596a | ||
|
|
fda405f35d | ||
|
|
4047cb82b9 | ||
|
|
7b57d22444 | ||
|
|
50e63bf83d | ||
|
|
8a9e257cdb | ||
|
|
3a05d8c2e8 | ||
|
|
8b222914c5 | ||
|
|
6d9182aba8 | ||
|
|
f9d3e419a0 | ||
|
|
4b0ecb1251 | ||
|
|
36a6af3266 | ||
|
|
2f592e6f62 | ||
|
|
860d4bb475 | ||
|
|
c7b5fb9187 | ||
|
|
5e0c9377f2 | ||
|
|
8db1ad6f19 | ||
|
|
f513db837d | ||
|
|
d7181ec6c2 | ||
|
|
7761f56d36 | ||
|
|
f136b9f6e7 | ||
|
|
12175f10a6 | ||
|
|
52dbe14af2 | ||
|
|
01bd09040e | ||
|
|
ab38e7069a | ||
|
|
9449642773 | ||
|
|
1f489a4537 | ||
|
|
6effcfb62e | ||
|
|
9bc95a6071 | ||
|
|
b80e80bd61 | ||
|
|
59fb1dea54 | ||
|
|
2cc4cc5deb | ||
|
|
4c5630ac43 | ||
|
|
3181bcc63e | ||
|
|
bf9cb33d63 | ||
|
|
e7aa6bbdf9 | ||
|
|
550b73ce23 | ||
|
|
ffaa756637 | ||
|
|
9cd67f06c1 | ||
|
|
79375616d5 | ||
|
|
ba6b8fbff1 | ||
|
|
6c37901824 | ||
|
|
0cf8661432 | ||
|
|
ff561157ac | ||
|
|
a7cf9fd74f | ||
|
|
788d24a455 | ||
|
|
f9e3870941 | ||
|
|
3a92fdd0f3 | ||
|
|
6a38defac5 | ||
|
|
3dfa7a8427 | ||
|
|
8dca835a8e | ||
|
|
f2278d47a5 | ||
|
|
de3f195187 | ||
|
|
f33c3ce21a | ||
|
|
67ba424a19 | ||
|
|
3376a08eb8 | ||
|
|
1ae15fe209 | ||
|
|
3c2820a5e1 | ||
|
|
8da6088f10 | ||
|
|
8961a266b3 | ||
|
|
db8ab80bef | ||
|
|
36efc359f7 | ||
|
|
4f4dab143e | ||
|
|
af1e31fb29 | ||
|
|
1f757a7378 | ||
|
|
6028172018 | ||
|
|
2d374160a1 | ||
|
|
587fbfb4ff | ||
|
|
84820a87c8 | ||
|
|
937d417e80 | ||
|
|
5b76cfd4dd | ||
|
|
957ed22b95 | ||
|
|
ebbe89cfb8 | ||
|
|
c8eb6f275f | ||
|
|
5f1213415b | ||
|
|
d36ccb2602 | ||
|
|
486b803856 | ||
|
|
d35630329f | ||
|
|
aa9f742852 | ||
|
|
960906e00c | ||
|
|
432acd2b0e | ||
|
|
84a03a81a0 | ||
|
|
f64791eadd | ||
|
|
51db76ac41 | ||
|
|
1ec7f71b6c | ||
|
|
bb3abdcc48 | ||
|
|
4d3ce038bc | ||
|
|
d66db547cb | ||
|
|
ebf1627753 | ||
|
|
bc818ca9cf | ||
|
|
0827124492 | ||
|
|
fea970f434 | ||
|
|
61cc14939b | ||
|
|
835d8c867e | ||
|
|
62601e4252 | ||
|
|
cc0b482c7a | ||
|
|
ff94f9ca0c | ||
|
|
8cd25dbd4f | ||
|
|
8ecf2e10c8 | ||
|
|
5fb13bb545 | ||
|
|
a24b745f5c | ||
|
|
a127ff8d89 | ||
|
|
d3aa27533a | ||
|
|
35ec9e0063 | ||
|
|
1bac98d086 | ||
|
|
fc2f759dd3 | ||
|
|
f337a13577 | ||
|
|
eebd89aedb | ||
|
|
a9573987ec | ||
|
|
be8c82870f | ||
|
|
e667da8453 | ||
|
|
1258466529 | ||
|
|
8495b223c6 | ||
|
|
8339e4a4cb | ||
|
|
fa7288e03e | ||
|
|
763b6fce6e | ||
|
|
7224acca84 | ||
|
|
e12e7c4170 | ||
|
|
0060d751b6 | ||
|
|
b368463670 | ||
|
|
21eb931701 | ||
|
|
e12b44ab2b | ||
|
|
723dc59490 | ||
|
|
5e1bc3e199 | ||
|
|
857548bbe4 | ||
|
|
56070f3017 | ||
|
|
f553efa69e | ||
|
|
84bf375f72 | ||
|
|
807455eb3d | ||
|
|
57284a9398 | ||
|
|
c8705a8022 | ||
|
|
bbdc4591df | ||
|
|
0a13e7943b | ||
|
|
1e0bc57d32 | ||
|
|
14e6cb05b3 | ||
|
|
b617bb0277 | ||
|
|
ac7b02a434 | ||
|
|
17458f3f4e | ||
|
|
e87fc8a839 | ||
|
|
6cf4d53b7c | ||
|
|
619545b993 | ||
|
|
e75d17f0e1 | ||
|
|
77a5b70576 | ||
|
|
59b0a00c6f | ||
|
|
5d24da4f2b | ||
|
|
0d3e96ee9f | ||
|
|
16480a6c44 | ||
|
|
813a59a36e | ||
|
|
23fd33030d | ||
|
|
2bdce4baa7 | ||
|
|
d2df0b7c84 | ||
|
|
16468b1216 | ||
|
|
3abdee5e87 | ||
|
|
d69a69da94 | ||
|
|
bc806bba34 | ||
|
|
81081ba2d4 | ||
|
|
5ad567803d | ||
|
|
486cafba9d | ||
|
|
c0a1119d1d | ||
|
|
bd4894ebd3 | ||
|
|
e9e95eda0a | ||
|
|
1b3f1ba2e8 | ||
|
|
5e27f981de | ||
|
|
c97f78bb39 | ||
|
|
f7b1032b7a | ||
|
|
7ee2649feb | ||
|
|
281320f2c4 | ||
|
|
30f7c74aee | ||
|
|
2dfd7257dd | ||
|
|
1f9dd5fd8c | ||
|
|
dd83c05a89 | ||
|
|
30cba053da | ||
|
|
5428d3f0b0 | ||
|
|
95398354e3 | ||
|
|
7e73216539 | ||
|
|
bbac1df463 | ||
|
|
208dd209a4 | ||
|
|
9139feaa30 | ||
|
|
f9f6c8b700 | ||
|
|
db8457af60 | ||
|
|
361dd00e9d | ||
|
|
7fd870cfd2 | ||
|
|
967ef99277 | ||
|
|
d93abe8e7d | ||
|
|
a4ba21f9db | ||
|
|
feabb20748 | ||
|
|
31fe06e3ce | ||
|
|
ef86bacf7f | ||
|
|
beabe48aec | ||
|
|
e335f51dbc | ||
|
|
38e422e84c | ||
|
|
1d6d11171d | ||
|
|
e32ced107e | ||
|
|
99d78ce9b8 | ||
|
|
066aff16f1 | ||
|
|
352dc6b311 | ||
|
|
a176ddbf3f | ||
|
|
1c6571d1db | ||
|
|
bf7b35230f | ||
|
|
46d901ada7 | ||
|
|
66f94d9452 | ||
|
|
62f428f434 | ||
|
|
713ad03c3b | ||
|
|
ad2ebc11dd | ||
|
|
72a0c4a487 | ||
|
|
fba5a35514 | ||
|
|
e2e5e40ea9 | ||
|
|
f5660667c8 | ||
|
|
64e225c4aa | ||
|
|
126491fc93 | ||
|
|
ea872d96f8 | ||
|
|
3f7202d89c | ||
|
|
2d3088ba27 | ||
|
|
469b602484 | ||
|
|
aa6f00f927 | ||
|
|
67afd8738b | ||
|
|
98d6170870 | ||
|
|
b2e1e5361f | ||
|
|
f96c80d7a1 | ||
|
|
7ae034d746 | ||
|
|
c409c146bf | ||
|
|
e0a7eb01cc | ||
|
|
3af2136770 | ||
|
|
13a2001a2b | ||
|
|
d6102284a4 | ||
|
|
c56ef8e0da | ||
|
|
bb17609ea3 | ||
|
|
f2f342e14c | ||
|
|
11bd07ee6b | ||
|
|
fa85dcbc4c | ||
|
|
473ae13a03 | ||
|
|
0561a28a4d | ||
|
|
b3467116fe | ||
|
|
e667d10eb8 | ||
|
|
a5bda00cdd | ||
|
|
627bc672bc | ||
|
|
9ef96080a6 | ||
|
|
bf4844e664 | ||
|
|
06f454abcf | ||
|
|
9d4e4a99bc | ||
|
|
b77be76f51 | ||
|
|
3121ed9a95 | ||
|
|
37dfd8fc12 | ||
|
|
de2719b0c5 | ||
|
|
e3d5abc9a2 | ||
|
|
ea7a5da1c1 | ||
|
|
2e063cc2d2 | ||
|
|
943509864d | ||
|
|
2ac228359f | ||
|
|
909f8da2ff | ||
|
|
5a5832394a | ||
|
|
52b60a22fd | ||
|
|
116da64e5c | ||
|
|
9c6c63c167 | ||
|
|
e7284262e4 | ||
|
|
decd9077e4 | ||
|
|
f6c47bf85e | ||
|
|
423191c13b | ||
|
|
7a5928d957 | ||
|
|
bbec3ae7da | ||
|
|
1f7daab677 | ||
|
|
91ab64dda9 | ||
|
|
722705468f | ||
|
|
07c920bad5 | ||
|
|
6d3ef11a7c | ||
|
|
4aabe9d946 | ||
|
|
7247b20686 | ||
|
|
8e8f618a22 | ||
|
|
a50af0ee64 | ||
|
|
436c334f5a | ||
|
|
4f84138ade | ||
|
|
a0d86ac5dc | ||
|
|
d426702213 | ||
|
|
e2be4f1275 | ||
|
|
c97610ad59 | ||
|
|
e8b5845174 | ||
|
|
c295584864 | ||
|
|
36257f73b9 | ||
|
|
9355a5ca24 | ||
|
|
d6447ef311 | ||
|
|
5e2a20fbe0 | ||
|
|
76823f7529 | ||
|
|
96a6a0d980 | ||
|
|
b05701be61 | ||
|
|
962ac97433 | ||
|
|
9491b81d0c | ||
|
|
316f08df08 | ||
|
|
f9554ec761 | ||
|
|
e128b1d750 | ||
|
|
e45efbcfb0 | ||
|
|
847ab96a48 | ||
|
|
1e52f790ad | ||
|
|
9bece712a9 | ||
|
|
6e0678e084 | ||
|
|
579cabdc1a | ||
|
|
9f252dfac4 | ||
|
|
5aad624346 | ||
|
|
23d1109910 | ||
|
|
ae2a72a810 | ||
|
|
ed096c3a1a | ||
|
|
123346ebdb | ||
|
|
8540965696 | ||
|
|
c8568b175b | ||
|
|
1737cbe1a5 | ||
|
|
c81048312d | ||
|
|
ac3afd5695 | ||
|
|
fa84813a37 | ||
|
|
8cd3807100 | ||
|
|
7aeb54d53d | ||
|
|
725ff41fb1 | ||
|
|
d071fe6d0c | ||
|
|
2234a763cb | ||
|
|
d52b65470e | ||
|
|
b63e697934 | ||
|
|
8a036c79c7 | ||
|
|
9a393fa793 | ||
|
|
7614f72df6 | ||
|
|
ef2db78567 | ||
|
|
8708468444 | ||
|
|
cd28a4fbcc | ||
|
|
e49881d1ed | ||
|
|
aa266f9b61 | ||
|
|
ccd3d0a3bf | ||
|
|
69fc367f69 | ||
|
|
a9f24542d5 | ||
|
|
8e4e458a2a | ||
|
|
feec7805af | ||
|
|
19f488095b | ||
|
|
cd65c6dd0e | ||
|
|
0c670cfdfd | ||
|
|
27ff1ac4f6 | ||
|
|
3f06de93f7 | ||
|
|
0da6495330 | ||
|
|
bf24347328 | ||
|
|
7a5d73f9df | ||
|
|
7fc403425d | ||
|
|
ef171bf2af | ||
|
|
b74a6624e3 | ||
|
|
19bf1fe56b | ||
|
|
ea6bb8dca3 | ||
|
|
9d6d3f96b2 | ||
|
|
5967c5d1d5 | ||
|
|
a6017c6ade | ||
|
|
2d3f2667ca | ||
|
|
ed90cadd75 | ||
|
|
0d9f34fd48 | ||
|
|
da55a3bdd2 | ||
|
|
333334e598 | ||
|
|
7168e4410c | ||
|
|
3ff8571f4a | ||
|
|
75ddcbbd01 | ||
|
|
91a44980f3 | ||
|
|
034f3c77ce | ||
|
|
9b3e18f333 | ||
|
|
fcb0a4a7e6 | ||
|
|
5a003a7cbe | ||
|
|
94e38cef9d | ||
|
|
d13d107aea | ||
|
|
4f87796e9c | ||
|
|
837da45f4f | ||
|
|
9e30f05e7d | ||
|
|
0df725112b | ||
|
|
098ed6b203 | ||
|
|
c6f9152efe | ||
|
|
9ea2029f81 | ||
|
|
2715f47a22 | ||
|
|
90d0b23441 | ||
|
|
b59e0a00a0 | ||
|
|
790571fd2c | ||
|
|
6ecebae110 | ||
|
|
849470a3c0 | ||
|
|
74d0a6f183 | ||
|
|
61c134215a | ||
|
|
b7218d8832 | ||
|
|
63e19427af | ||
|
|
d4f8578fd6 | ||
|
|
eaccd062d3 | ||
|
|
5f2d5931f4 | ||
|
|
ce40fa608e | ||
|
|
0979c75852 | ||
|
|
f01e8e0866 | ||
|
|
a4e303ab63 | ||
|
|
c5137c9c29 | ||
|
|
9bce88f9b1 | ||
|
|
68c70effec | ||
|
|
ebae218219 | ||
|
|
6685b759b2 | ||
|
|
539b0496bf | ||
|
|
3c33fac8f4 | ||
|
|
9613f76ef5 | ||
|
|
3f0d344313 | ||
|
|
823ee63c25 | ||
|
|
d5d76f9c63 | ||
|
|
dfc9a6fbb4 | ||
|
|
2bd9aece35 | ||
|
|
21870c9fa2 | ||
|
|
7c315b3afd | ||
|
|
da0cdb081d | ||
|
|
9bd0a3f1c9 | ||
|
|
83992895e4 | ||
|
|
aa3b336e46 | ||
|
|
e17b374fde | ||
|
|
61158b62f1 | ||
|
|
88ed43a92e | ||
|
|
e5fff6b452 | ||
|
|
044d49c53a | ||
|
|
69abf8d9b1 | ||
|
|
14e13899a6 | ||
|
|
488c246222 | ||
|
|
654905a79c | ||
|
|
12cb199803 | ||
|
|
8759cf726b | ||
|
|
7a45c9e434 | ||
|
|
9ee69dea55 | ||
|
|
ebe38e977f | ||
|
|
40ad143c3e | ||
|
|
875159fa5f | ||
|
|
c97c65de34 | ||
|
|
25ae09b2c5 | ||
|
|
853c2b4b85 | ||
|
|
682db1ca75 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ node_modules*
|
||||
config.status*
|
||||
config/environments/*.js
|
||||
.idea
|
||||
.vscode
|
||||
tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
|
||||
29
.jshintrc
29
.jshintrc
@@ -7,8 +7,8 @@
|
||||
// // Enforcing
|
||||
// "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
// "camelcase" : false, // true: Identifiers must be in camelCase
|
||||
// "curly" : true, // true: Require {} for every new block or scope
|
||||
// "eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
@@ -31,7 +31,7 @@
|
||||
// "maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
// "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
// "maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : 8, // {int} Max cyclomatic complexity per function
|
||||
"maxcomplexity" : 6, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : 120, // {int} Max number of characters per line
|
||||
//
|
||||
// // Relaxing
|
||||
@@ -82,17 +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,
|
||||
"suite": true,
|
||||
"suiteSetup": true,
|
||||
"test": true,
|
||||
"suiteTeardown": true
|
||||
}
|
||||
// Custom predefined global variables
|
||||
"predef": [
|
||||
"-console", // disallows console, use debug
|
||||
"beforeEach",
|
||||
"afterEach",
|
||||
"before",
|
||||
"after",
|
||||
"describe",
|
||||
"it"
|
||||
]
|
||||
}
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,11 +1,19 @@
|
||||
dist: trusty
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
postgresql: "9.5"
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
- postgresql-plpython-9.5
|
||||
- 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
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
env:
|
||||
@@ -14,9 +22,3 @@ env:
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#cartodb"
|
||||
use_notice: true
|
||||
|
||||
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Contributing
|
||||
---
|
||||
|
||||
The issue tracker is at [github.com/CartoDB/Windshaft-cartodb](https://github.com/CartoDB/Windshaft-cartodb).
|
||||
|
||||
We love pull requests from everyone, see [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/#contributing).
|
||||
|
||||
|
||||
## Submitting Contributions
|
||||
|
||||
* You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://cartodb.com/contributing).
|
||||
@@ -1,12 +1,11 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. Drop npm-shrinkwrap.json
|
||||
5. Run npm shrinkwrap to recreate npm-shrinkwrap.json
|
||||
6. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
7. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
8. Announce on cartodb@googlegroups.com
|
||||
9. Stub NEWS/package for next version
|
||||
4. Recreate npm-shrinkwrap.json with: `npm install --no-shrinkwrap && npm shrinkwrap`
|
||||
5. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
6. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
7. Announce on cartodb@googlegroups.com
|
||||
8. Stub NEWS/package for next version
|
||||
|
||||
Versions:
|
||||
|
||||
|
||||
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.
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2014, Vizzuality
|
||||
Copyright (c) 2015, CartoDB
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
||||
26
Makefile
26
Makefile
@@ -18,20 +18,32 @@ config.status--test:
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
|
||||
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
|
||||
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
|
||||
TEST_SUITE_INTEGRATION := $(shell find test/integration -name "*.js")
|
||||
TEST_SUITE_ACCEPTANCE := $(shell find test/acceptance -name "*.js")
|
||||
|
||||
test: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} \
|
||||
test/unit/cartodb/*.js \
|
||||
test/unit/cartodb/cache/model/*.js \
|
||||
test/integration/*.js \
|
||||
test/acceptance/*.js \
|
||||
test/acceptance/cache/*.js
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE)
|
||||
|
||||
test-unit: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_UNIT)
|
||||
|
||||
test-integration: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_INTEGRATION)
|
||||
|
||||
test-acceptance: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_ACCEPTANCE)
|
||||
|
||||
jshint:
|
||||
@echo "***jshint***"
|
||||
@./node_modules/.bin/jshint lib/ test/ app.js
|
||||
|
||||
test-all: jshint test
|
||||
test-all: test jshint
|
||||
|
||||
coverage:
|
||||
@RUNTESTFLAGS=--with-coverage make test
|
||||
|
||||
57
README.md
57
README.md
@@ -11,28 +11,11 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
|
||||
* gets the default geometry type from the cartodb redis store
|
||||
* allows tiles to be styled individually
|
||||
* provides a link to varnish high speed cache
|
||||
* provides a ``infowindow`` endpoint for windshaft (DEPRECATED)
|
||||
* provides a ``map_metadata`` endpoint for windshaft (DEPRECATED)
|
||||
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See Installing Mapnik.
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- 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
|
||||
---------
|
||||
@@ -43,23 +26,15 @@ see ```./configure --help``` to see available options.
|
||||
|
||||
Look at lib/cartodb/server_options.js for more on config
|
||||
|
||||
Build/install
|
||||
-------------
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
Checkout your commit/branch. If you need to reinstall dependencies (you can check [NEWS](NEWS.md)) do the following:
|
||||
|
||||
```
|
||||
git clone
|
||||
npm install
|
||||
rm -rf node_modules; 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.
|
||||
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
@@ -87,3 +62,21 @@ Examples
|
||||
--------
|
||||
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
|
||||
Contributing
|
||||
---
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
### Developing with a custom windshaft version
|
||||
|
||||
If you plan or want to use a custom / not released yet version of windshaft (or any other dependency) the best option is
|
||||
to use `npm link`. You can read more about it at [npm-link: Symlink a package folder](https://docs.npmjs.com/cli/link).
|
||||
|
||||
**Quick start**:
|
||||
|
||||
```shell
|
||||
~/windshaft-directory $ npm install
|
||||
~/windshaft-directory $ npm link
|
||||
~/windshaft-cartodb-directory $ npm link windshaft
|
||||
```
|
||||
|
||||
159
app.js
159
app.js
@@ -1,111 +1,134 @@
|
||||
/*
|
||||
* Windshaft-CartoDB
|
||||
* ===============
|
||||
*
|
||||
* ./app.js [environment]
|
||||
*
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var ENV;
|
||||
if ( process.argv[2] ) {
|
||||
ENV = process.argv[2];
|
||||
} else if ( process.env.NODE_ENV ) {
|
||||
ENV = process.env.NODE_ENV;
|
||||
} else {
|
||||
ENV = 'development';
|
||||
}
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
process.env.NODE_ENV = ENV;
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
.example(
|
||||
'$0 production -c /etc/sql-api/config.js',
|
||||
'start server in production environment with /etc/sql-api/config.js as config file'
|
||||
)
|
||||
.alias('h', 'help')
|
||||
.alias('c', 'config')
|
||||
.nargs('c', 1)
|
||||
.describe('c', 'Load configuration from path')
|
||||
.argv;
|
||||
|
||||
// sanity check
|
||||
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
console.error("\nnode app.js [environment]");
|
||||
console.error("environments: development, production, staging\n");
|
||||
var environmentArg = argv._[0] || process.env.NODE_ENV || 'development';
|
||||
var configurationFile = path.resolve(argv.config || './config/environments/' + environmentArg + '.js');
|
||||
if (!fs.existsSync(configurationFile)) {
|
||||
logError('Configuration file "%s" does not exist', configurationFile);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
global.environment = require(configurationFile);
|
||||
var ENVIRONMENT = argv._[0] || process.env.NODE_ENV || global.environment.environment;
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole:true
|
||||
var availableEnvironments = {
|
||||
production: true,
|
||||
staging: true,
|
||||
development: true
|
||||
};
|
||||
|
||||
// sanity check
|
||||
if (!availableEnvironments[ENVIRONMENT]){
|
||||
logError('node app.js [environment]');
|
||||
logError('environments: %s', Object.keys(availableEnvironments).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
if (global.environment.uv_threadpool_size) {
|
||||
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
|
||||
}
|
||||
|
||||
// set global HTTP and HTTPS agent default configurations
|
||||
// ref https://nodejs.org/api/http.html#http_new_agent_options
|
||||
var agentOptions = _.defaults(global.environment.httpAgent || {}, {
|
||||
keepAlive: false,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: Infinity,
|
||||
maxFreeSockets: 256
|
||||
});
|
||||
http.globalAgent = new http.Agent(agentOptions);
|
||||
https.globalAgent = new https.Agent(agentOptions);
|
||||
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4jsConfig = {
|
||||
appenders: [],
|
||||
replaceConsole: true
|
||||
};
|
||||
|
||||
if ( global.environment.log_filename ) {
|
||||
var logdir = path.dirname(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);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Logs will be written to " + global.environment.log_filename);
|
||||
log4js_config.appenders.push(
|
||||
{ type: "file", filename: global.environment.log_filename }
|
||||
);
|
||||
var logFilename = path.resolve(global.environment.log_filename);
|
||||
var logDirectory = path.dirname(logFilename);
|
||||
if (!fs.existsSync(logDirectory)) {
|
||||
logError("Log filename directory does not exist: " + logDirectory);
|
||||
process.exit(1);
|
||||
}
|
||||
log("Logs will be written to " + logFilename);
|
||||
log4jsConfig.appenders.push(
|
||||
{ type: "file", absolute: true, filename: logFilename }
|
||||
);
|
||||
} else {
|
||||
log4js_config.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
log4jsConfig.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
}
|
||||
|
||||
global.log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
|
||||
redisPool = new RedisPool(redisOpts);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var cartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
var cartodbWindshaft = require('./lib/cartodb/server');
|
||||
var serverOptions = require('./lib/cartodb/server_options');
|
||||
|
||||
var ws = cartodbWindshaft(serverOptions);
|
||||
|
||||
if (global.statsClient) {
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
}
|
||||
var server = cartodbWindshaft(serverOptions);
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good number if you have up to 1024 filedescriptors
|
||||
// 4 is good if you have max 32 filedescriptors
|
||||
// 1 is good if you have max 16 filedescriptors
|
||||
ws.maxConnections = global.environment.maxConnections || 128;
|
||||
var backlog = global.environment.maxConnections || 128;
|
||||
|
||||
ws.listen(global.environment.port, global.environment.host);
|
||||
var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
|
||||
|
||||
var version = require("./package").version;
|
||||
|
||||
ws.on('listening', function() {
|
||||
console.log(
|
||||
"Windshaft tileserver %s started on %s:%s (%s)",
|
||||
version, global.environment.host, global.environment.port, ENV
|
||||
);
|
||||
listener.on('listening', function() {
|
||||
log('Using configuration file "%s"', configurationFile);
|
||||
log(
|
||||
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
|
||||
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
|
||||
);
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
var memoryUsage = process.memoryUsage();
|
||||
Object.keys(memoryUsage).forEach(function(k) {
|
||||
global.statsClient.gauge('windshaft.memory.' + k, memoryUsage[k]);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'development'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -10,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
|
||||
@@ -20,6 +23,21 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'http://localhost.lan:{{=it.port}}/user/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -30,7 +48,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
|
||||
@@ -59,6 +77,7 @@ var config = {
|
||||
extent: "-180,-90,180,90",
|
||||
srid: 4326,
|
||||
*/
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -86,16 +105,39 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
@@ -125,7 +167,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
|
||||
@@ -140,8 +203,46 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: '/tmp/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-dev/millstone-dev'
|
||||
@@ -170,7 +271,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
@@ -191,7 +301,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
|
||||
@@ -209,7 +319,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
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -10,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
|
||||
@@ -20,6 +23,21 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -31,7 +49,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])'
|
||||
@@ -53,6 +71,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
@@ -80,16 +99,39 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
@@ -119,7 +161,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
|
||||
@@ -134,8 +197,46 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
@@ -164,7 +265,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
@@ -185,7 +295,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: {
|
||||
@@ -209,7 +319,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -10,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
|
||||
@@ -20,6 +23,21 @@ var config = {
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -31,7 +49,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]'
|
||||
@@ -53,6 +71,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -80,16 +99,39 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
@@ -119,7 +161,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
|
||||
@@ -134,8 +197,46 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
@@ -164,7 +265,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
@@ -185,7 +295,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: {
|
||||
@@ -209,7 +319,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ var config = {
|
||||
environment: 'test'
|
||||
,port: 8888
|
||||
,host: '127.0.0.1'
|
||||
// Size of the threadpool which can be used to run user code and get notified in the loop thread
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
@@ -10,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
|
||||
@@ -20,6 +23,20 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -31,7 +48,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])'
|
||||
@@ -53,6 +70,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -80,16 +98,39 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// tilelive-mapnik uses an internal cache to store tiles/grids
|
||||
// generated when using metatile. This options allow to tune
|
||||
// the behaviour for that internal cache.
|
||||
metatileCache: {
|
||||
// Time an object must stay in the cache until is removed
|
||||
ttl: 0,
|
||||
// Whether an object must be removed after the first hit
|
||||
// Usually you want to use `true` here when ttl>0.
|
||||
deleteOnHit: false
|
||||
},
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
@@ -119,6 +160,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: {
|
||||
@@ -136,8 +197,46 @@ var config = {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'node-windshaft.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-test/millstone'
|
||||
@@ -166,7 +265,16 @@ var config = {
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
},
|
||||
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
|
||||
}
|
||||
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
|
||||
,httpAgent: {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 25,
|
||||
maxFreeSockets: 256
|
||||
}
|
||||
,varnish: {
|
||||
host: '',
|
||||
@@ -187,7 +295,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
|
||||
@@ -205,7 +313,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
10
configure
vendored
10
configure
vendored
@@ -20,7 +20,6 @@
|
||||
ENVDIR=config/environments
|
||||
|
||||
PGPORT=
|
||||
SQLAPI_PORT=
|
||||
MAPNIK_VERSION=
|
||||
ENVIRONMENT=development
|
||||
|
||||
@@ -32,7 +31,6 @@ usage() {
|
||||
echo "Configuration:"
|
||||
echo " --help display this help and exit"
|
||||
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
|
||||
echo " --with-sqlapi-port=NUM access SQL-API server on TCP port NUM [$SQLAPI_PORT]"
|
||||
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
|
||||
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
|
||||
}
|
||||
@@ -46,9 +44,6 @@ while test -n "$1"; do
|
||||
--with-pgport=*)
|
||||
PGPORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-sqlapi-port=*)
|
||||
SQLAPI_PORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-mapnik-version=*)
|
||||
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
@@ -67,12 +62,8 @@ ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
|
||||
if [ -z "$PGPORT" ]; then
|
||||
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
|
||||
fi
|
||||
if [ -z "$SQLAPI_PORT" ]; then
|
||||
SQLAPI_PORT=`node -e "console.log(require('${ENVEX}').sqlapi.port)"`
|
||||
fi
|
||||
|
||||
echo "PGPORT: $PGPORT"
|
||||
echo "SQLAPI_PORT: $SQLAPI_PORT"
|
||||
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
|
||||
echo "ENVIRONMENT: $ENVIRONMENT"
|
||||
|
||||
@@ -82,7 +73,6 @@ echo "Writing $o"
|
||||
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
|
||||
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "${ENVEX}" \
|
||||
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
|
||||
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
|
||||
> "$o"
|
||||
|
||||
STATUSFILE=config.status--${ENVIRONMENT}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# Kind of maps
|
||||
|
||||
Windshaft-CartoDB supports the following types of maps:
|
||||
|
||||
- [Temporary maps](#temporary-maps) (created by anyone)
|
||||
- [Detached maps](#detached-maps)
|
||||
- [Inline maps](#inline-maps) (legacy)
|
||||
- [Persistent maps](#peristent-maps) (created by CartDB user)
|
||||
- [Template maps](#template-maps)
|
||||
- [Table maps](#table-maps) (legacy, deprecated)
|
||||
|
||||
## Temporary maps
|
||||
|
||||
Temporary maps have no owners and are anonymous in nature.
|
||||
There are two kinds of temporary maps:
|
||||
|
||||
- Detached maps (aka MultiLayer-API)
|
||||
- Inline maps
|
||||
|
||||
### Detached maps
|
||||
|
||||
Detached maps are maps that are configured with a request
|
||||
obtaining a temporary token and then used by referencing
|
||||
the obtained token. The token expires automatically when unused.
|
||||
|
||||
Anyone can create detached maps, but users will need read access
|
||||
to the data source of the map layers.
|
||||
|
||||
The configuration format is a [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
|
||||
|
||||
The HTTP endpoints for creating the map and using it are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
|
||||
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
### Inline maps
|
||||
|
||||
Inline maps are maps that only exist for a single request,
|
||||
being the request for a specific map resource (tile).
|
||||
|
||||
Inline maps are always bound to a table, and can only be
|
||||
obtained by those having read access to the that table.
|
||||
Additionally, users need to have access to any datasource
|
||||
specified as part of the configuration.
|
||||
|
||||
Inline maps only support PNG and UTF8GRID tiles.
|
||||
|
||||
The configuration consist in a set of parameters, to be
|
||||
specified in the query string of the tile request:
|
||||
|
||||
* sql - the query to run as datasource, can be an array
|
||||
* style - the CartoCSS style for the datasource, can be an array
|
||||
* style_version - version of the CartoCSS style, can be an array
|
||||
* interactivity - only for fetching UTF8GRID,
|
||||
|
||||
If the style is not provided, style of the associated table is
|
||||
used; if the sql is not provided, all records of the associated
|
||||
table are used as the datasource; the two possibilities result
|
||||
in a mix between _inline_ maps and [Table maps][].
|
||||
|
||||
*TODO* specify (or link) api endpoints
|
||||
|
||||
## Persistent maps
|
||||
|
||||
Persistent maps can only be created by a CartoDB user who has full
|
||||
responsibility over editing and deleting them. There are two
|
||||
kind of persistent maps:
|
||||
|
||||
- Template maps
|
||||
- Table maps (legacy, deprecated)
|
||||
|
||||
### Templated maps
|
||||
|
||||
Templated maps are templated [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
|
||||
associated with an authorization certificate.
|
||||
|
||||
The authorization certificate determines who can instanciate the
|
||||
template and use the resulting map. Authorized users of the instanciated
|
||||
maps will have the same database access privilege of the template owner.
|
||||
|
||||
The HTTP endpoints for creating and using templated maps are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
|
||||
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
### Table maps
|
||||
|
||||
Table maps are maps associated with a table.
|
||||
Configuration of such maps is limited to the CartoCSS style.
|
||||
|
||||
* style - the CartoCSS style for the datasource, can be an array
|
||||
* style_version - version of the CartoCSS style, can be an array
|
||||
|
||||
You can only fetch PNG or UTF8GRID tiles from these maps.
|
||||
|
||||
Access method is the same as the one for [Inline maps](#inline-maps)
|
||||
|
||||
# Endpoints description
|
||||
|
||||
- **/api/maps/** (same interface than https://github.com/CartoDB/Windshaft/wiki/Multilayer-API)
|
||||
- **/api/maps/named** (same interface than https://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
|
||||
|
||||
|
||||
NOTE: in case Multilayer-API does not contain this info yet, the
|
||||
endpoint for fetching attributes is this:
|
||||
|
||||
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
|
||||
- would return { c: 1, d: 2 }
|
||||
|
||||
867
docs/Map-API.md
867
docs/Map-API.md
@@ -1,862 +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 CARTO Maps API allows you to generate maps based on data hosted in your CARTO 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 CARTO 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 CARTO.js example](/carto-engine/carto-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.0.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": "cartodb",
|
||||
"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` and the timestamp (`last_updated`) of the last data modification.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
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.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"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.19.1/doc/MapConfig-1.1.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** *(optional)*
|
||||
Includes information about the layers. Some layers may not have metadata.
|
||||
|
||||
- **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"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The tiles can be accessed using:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
For 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 }
|
||||
```
|
||||
|
||||
Notice UTF Grid and attributes endpoints need an integer parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. In this case, 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
|
||||
|
||||
### 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 %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter, and only contain letters, numbers, 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/master/doc/MapConfig-1.1.0.md) for more info.
|
||||
|
||||
#### 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
|
||||
{
|
||||
"error": "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
|
||||
{
|
||||
"error": "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
|
||||
{
|
||||
"error": "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
|
||||
{
|
||||
"error": "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
|
||||
{
|
||||
"error": "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 `layergroupsid token` calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
##### 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`
|
||||
```
|
||||
|
||||
####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**
|
||||
|
||||
```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"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Additoinally, 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
|
||||
<div clas="wrap"><p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>,</div>
|
||||
|
||||
####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)
|
||||
|
||||
93
docs/MapConfig-Analyses-extension.md
Normal file
93
docs/MapConfig-Analyses-extension.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.4.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.4.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension targets layers with `sql` option, including layer types: `cartodb`, `mapnik`, and `torque`.
|
||||
|
||||
It extends MapConfig with a new attribute: `analyses`.
|
||||
|
||||
## 2.1 Analyses attribute
|
||||
|
||||
The new analyses attribute must be an array of analyses as per [camshaft](https://github.com/CartoDB/camshaft). Each
|
||||
analysis must adhere to the [camshaft-reference](https://github.com/CartoDB/camshaft/blob/0.8.0/reference/versions/0.7.0/reference.json) specification.
|
||||
|
||||
Each node can have an id that can be later references to consume the query from MapConfig's layers.
|
||||
|
||||
Basic analyses example:
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `id` free identifier that can be reference from any layer
|
||||
"id": "HEAD",
|
||||
// REQUIRED
|
||||
// string, `type` camshaft's analysis type
|
||||
"type": "source",
|
||||
// REQUIRED
|
||||
// object, `params` will depend on `type`, check camshaft-reference for more information
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# 2.2. Integration with layers
|
||||
|
||||
As pointed before an analysis node id can be referenced from layers to consume its output query.
|
||||
|
||||
The layer consuming the output must reference it with the following option:
|
||||
|
||||
```
|
||||
{
|
||||
"options": {
|
||||
// REQUIRED
|
||||
// object, `source` as in the future we might want to have other source options
|
||||
"source": {
|
||||
// REQUIRED
|
||||
// string, `id` the analysis node identifier
|
||||
"id": "HEAD"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "...",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyses": [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
314
docs/MapConfig-Dataviews-extension.md
Normal file
314
docs/MapConfig-Dataviews-extension.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.4.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.4.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
|
||||
|
||||
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
|
||||
|
||||
## 2.1. Dataview types
|
||||
|
||||
### List
|
||||
|
||||
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the list type
|
||||
“type”: “list”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// array, `columns` to select for the list
|
||||
“columns”: [“name”, “description”]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "list",
|
||||
"rows": [
|
||||
{
|
||||
"{columnName1}": "val1",
|
||||
"{columnName2}": 100
|
||||
},
|
||||
{
|
||||
"{columnName1}": "val2",
|
||||
"{columnName2}": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the aggregation type
|
||||
“type”: “aggregation”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “country”,
|
||||
// REQUIRED
|
||||
// string, `aggregation` operation to perform
|
||||
“aggregation”: “count”
|
||||
// OPTIONAL
|
||||
// string, `aggregationColumn` column value to aggregate
|
||||
// This param is required when `aggregation` is different than "count"
|
||||
“aggregationColumn”: “population”
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "aggregation",
|
||||
"categories": [
|
||||
{
|
||||
"category": "foo",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"category": "bar",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Histograms
|
||||
|
||||
Histograms represent the data distribution for a column.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the histogram type
|
||||
“type”: “histogram”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “name”,
|
||||
// OPTIONAL
|
||||
// number, `bins` how many buckets the histogram should use
|
||||
“bins”: 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "histogram",
|
||||
"bins": [{"bin": 0, "start": 2, "end": 2, "min": 2, "max": 2, "freq": 1}, null, null, {"bin": 3, "min": 40, "max": 44, "freq": 2}, null],
|
||||
"width": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Formula
|
||||
|
||||
Formulas given a final value representing the whole dataset.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the formula type
|
||||
“type”: “formula”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “name”,
|
||||
// REQUIRED
|
||||
// string, `aggregation` operation to perform
|
||||
“operation”: “count”
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Operation must be: “min”, “max”, “count”, “avg”, or “sum”.
|
||||
|
||||
Result
|
||||
```
|
||||
{
|
||||
"type": "formula",
|
||||
"operation": "count",
|
||||
"result": 1000,
|
||||
"nulls": 0
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 2.2 Dataviews attribute
|
||||
|
||||
The new dataviews attribute must be a dictionary of dataviews.
|
||||
|
||||
An analysis node id can be referenced from dataviews to consume its output query.
|
||||
|
||||
|
||||
The layer consuming the output must reference it with the following option:
|
||||
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// object, `source` as in the future we might want to have other source options
|
||||
"source": {
|
||||
// REQUIRED
|
||||
// string, `id` the analysis node identifier
|
||||
"id": "HEAD"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "...",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataviews" {
|
||||
"basic_histogram": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"type": "histogram",
|
||||
"options": {
|
||||
"column": "pop_max"
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyses": [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Filters
|
||||
|
||||
Camshaft's analyses expose a filtering capability and `aggregation` and `histogram` dataviews get them for free with
|
||||
this extension. Filters are available with the very dataview id, so if you have a "basic_histogram" histogram dataview
|
||||
you can filter with a range filter with "basic_histogram" name.
|
||||
|
||||
|
||||
## 3.1 Filter types
|
||||
|
||||
### Category
|
||||
|
||||
Allows to remove results that are not contained within a set of elements.
|
||||
Initially this filter can be applied to a `numeric` or `text` columns.
|
||||
|
||||
Params
|
||||
|
||||
```
|
||||
{
|
||||
“accept”: [“Spain”, “Germany”]
|
||||
“reject”: [“Japan”]
|
||||
}
|
||||
```
|
||||
|
||||
### Range filter
|
||||
|
||||
Allows to remove results that don’t satisfy numeric min and max values.
|
||||
Filter is applied to a numeric column.
|
||||
|
||||
Params
|
||||
|
||||
```
|
||||
{
|
||||
“min”: 0,
|
||||
“max”: 1000
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2. How to apply filters
|
||||
|
||||
Filters must be applied at map instantiation time.
|
||||
|
||||
With :mapconfig as a valid MapConfig and with :filters (a valid JSON) as:
|
||||
|
||||
### Anonymous map
|
||||
|
||||
`GET /api/v1/map?config=:mapconfig&filters=:filters`
|
||||
|
||||
`POST /api/v1/map?filters=:filters`
|
||||
with `BODY=:mapconfig`
|
||||
|
||||
If in the future we need to support a bigger filters param and it doesn’t fit in the query string,
|
||||
we might solve it by accepting:
|
||||
|
||||
`POST /api/v1/map`
|
||||
with `BODY={“config”: :mapconfig, “filters”: :filters}`
|
||||
|
||||
### Named map
|
||||
|
||||
Assume :params (a valid JSON) as named maps params, like in: `{“color”: “red”}`
|
||||
|
||||
`GET /api/v1/named/:name/jsonp?config=:params&filters=:filters&callback=cb`
|
||||
|
||||
`POST /api/v1/named/:name?filters=:filters`
|
||||
with `BODY=:params`
|
||||
|
||||
If, again, in the future we need to support a bigger filters param that doesn’t fit in the query string,
|
||||
we might solve it by accepting:
|
||||
|
||||
`POST /api/v1/named/:name`
|
||||
with `BODY={“config”: :params, “filters”: :filters}`
|
||||
|
||||
|
||||
## 3.3 Bounding box special filter
|
||||
|
||||
A bounding box filter allows to remove results that don’t satisfy a geospatial range.
|
||||
|
||||
The bounding box special filter is available per dataview and there is no need to create a bounding box definition as
|
||||
it’s always possible to apply a bbox filter per dataview.
|
||||
|
||||
A dataview can get its result filtered by bounding box by sending a bbox param in the query string,
|
||||
param must be in the form `west,south,east,north`.
|
||||
|
||||
So applying a bbox filter to a dataview looks like:
|
||||
GET /api/v1/map/:layergroupid/dataview/:dataview_name?bbox=-90,-45,90,45
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0-alpha
|
||||
|
||||
- WIP document
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/CartoDB/Windshaft/blob/master/doc/Multilayer-API.md) in a few ways.
|
||||
|
||||
## Last modification timestamp embedded in the token
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
117
docs/Routes.md
117
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,44 +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 /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
|
||||
|
||||
@@ -72,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 477a4c2..f69eebb 100644
|
||||
--- a/lib/cartodb/cartodb_windshaft.js
|
||||
+++ b/lib/cartodb/cartodb_windshaft.js
|
||||
@@ -242,6 +242,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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
Template maps are layergroup configurations that rather than being
|
||||
fully defined contain variables that can be set to produce a different
|
||||
layergroup configurations (instantiation).
|
||||
|
||||
Template maps are persistent, can only be created and deleted by the
|
||||
CartoDB user showing a valid API_KEY.
|
||||
|
||||
|
||||
# Template format
|
||||
|
||||
A templated layergroup would allow using placeholders
|
||||
in the "cartocss" and "sql" elements in the "option"
|
||||
field of any "layer" of a layergroup configuration
|
||||
(see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification).
|
||||
|
||||
Valid placeholder names start with a letter and can only
|
||||
contain letters, numbers or underscores. They have to be
|
||||
written between ``<%= `` and `` %>`` strings in order to be
|
||||
replaced. Example: ``<%= my_color %>``.
|
||||
|
||||
The set of supported placeholders for a template will need to be
|
||||
explicitly defined specifying type and default value for each.
|
||||
|
||||
**placeholder types**
|
||||
|
||||
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)
|
||||
* ... (add more as need arises)
|
||||
|
||||
Placeholder default value will be used when not provided at
|
||||
instantiation time and could be used to test validity of the
|
||||
template by creating a default instance.
|
||||
|
||||
```js
|
||||
// template.json
|
||||
{
|
||||
version: "0.0.1",
|
||||
// there can be at most 1 template with the same name for any user
|
||||
// valid names start with a letter and only contains letter, numbers
|
||||
// or underscores
|
||||
name: "template_name",
|
||||
// embedded authorization certificate
|
||||
auth: {
|
||||
method: "token", // or "open" (the default if no "method" is given)
|
||||
// only (required and non empty) for "token" method
|
||||
valid_tokens: ["auth_token1","auth_token2"]
|
||||
},
|
||||
// Variables not listed here are not substituted
|
||||
// Variable not provided at instantiation time trigger an error
|
||||
// A default is required for optional variables
|
||||
// Type specification is used for quoting, to avoid injections
|
||||
placeholders: {
|
||||
color: {
|
||||
type:"css_color",
|
||||
default:"red"
|
||||
},
|
||||
cartodb_id: {
|
||||
type:"number",
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
// see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
|
||||
"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 %>"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Creating a templated map
|
||||
|
||||
You can create a template map with a single call (for simplicity).
|
||||
|
||||
You'd use a POST sending JSON data:
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name exists in the user storage,
|
||||
a 400 response is generated.
|
||||
|
||||
Errors are in this form:
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Updating an existing template
|
||||
|
||||
You can update a template map with a PUT:
|
||||
|
||||
```sh
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
|
||||
```
|
||||
A template with the same name will be updated, if any.
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name does NOT exist,
|
||||
a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Listing available templates
|
||||
|
||||
You can get a list of available templates with a GET to ``/template``.
|
||||
A valid api_key is required.
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Getting a specific template
|
||||
|
||||
You can get the definition of a template with a
|
||||
GET to ``/template/:template_name``.
|
||||
A valid api_key is required.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Instantiating a template map
|
||||
|
||||
You can instantiate a template map passing all required parameters with
|
||||
a POST to ``/template/:template_name``.
|
||||
|
||||
Valid credentials will be needed, if required by the template.
|
||||
|
||||
```js
|
||||
// params.js
|
||||
{
|
||||
color: '#ff0000',
|
||||
cartodb_id: 3
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.js \
|
||||
'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"layergroupid":"docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated":"2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the ``layergroupid`` for fetching tiles and grids as you do
|
||||
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
|
||||
But you'll still have to show the ``auth_token``, if required by the template.
|
||||
|
||||
### using JSONP
|
||||
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
|
||||
|
||||
```
|
||||
curl 'https://docs.cartodb.com/tiles/template/@template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
|
||||
```
|
||||
|
||||
it takes the ``callback`` function (required), ``auth_token`` in case the template needs auth and ``config`` which is the variabñes for the template (in case it has variables). For example config may be created (using javascript)
|
||||
```
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
the response it's in this format:
|
||||
```
|
||||
jQuery17205720721024554223_1390996319118(
|
||||
{
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
}
|
||||
)
|
||||
```
|
||||
# Deleting a template map
|
||||
|
||||
You can delete a templated map with a DELETE to ``/template/:template_name``:
|
||||
|
||||
```sh
|
||||
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response would be issued.
|
||||
Otherwise a 4xx response with this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
191
docs/anonymous_maps.md
Normal file
191
docs/anonymous_maps.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 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.carto.com/carto-engine/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}.carto.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}.carto.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}.carto.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}.carto.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}.carto.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.carto.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}.carto.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}.carto.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}.carto.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.
|
||||
- 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}.carto.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 CARTO. 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.
|
||||
568
docs/named_maps.md
Normal file
568
docs/named_maps.md
Normal file
@@ -0,0 +1,568 @@
|
||||
# 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 CARTO servers, to select data from your CARTO 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 CARTO.js. This is achieved by adding the [`namedmap` type](http://docs.carto.com/carto-engine/carto-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 CARTO 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.carto.com/carto-engine/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.carto.com/carto-engine/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.carto.com/carto-engine/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
|
||||
},
|
||||
"preview_layers": {
|
||||
"0": true,
|
||||
"layer1": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 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.carto.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.carto.com/carto-engine/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. Also it is possible to choose which layers are visible or not with `preview_layers` indicating its visibility by layer index or id (visible by default).
|
||||
--- | ---
|
||||
|_ 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.carto.com/carto-engine/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}.carto.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 CARTO.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}.carto.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.carto.com/carto-engine/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}.carto.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}.carto.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}.carto.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}.carto.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}.carto.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"
|
||||
})
|
||||
```
|
||||
|
||||
## CARTO.js for Named Maps
|
||||
|
||||
You can use a Named Map that you created (which is defined by its `name`), to create a map using CARTO.js. This is achieved by adding the [`namedmap` type](http://docs.carto.com/carto-engine/carto-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.
|
||||
|
||||
[CARTO.js](http://docs.carto.com/carto-engine/carto-js/) has methods for accessing your Named Maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.carto.com/carto-engine/carto-js/api-methods/#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
|
||||
**Note:** The CARTO.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.carto.com/carto-engine/carto-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 CARTO.js:
|
||||
|
||||
```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 CARTO.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}.carto.com/api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.png"
|
||||
|
||||
The placeholders indicate the following:
|
||||
|
||||
- [`template_id`](http://docs.carto.com/carto-engine/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}.carto.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}.carto.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}.carto.com/api/v1/map/named/{template_id}/all/{z}/{x}/{y}.png"
|
||||
- To show a [list of layers](http://docs.carto.com/carto-engine/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}.carto.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.carto.com/carto-engine/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}.carto.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://{username}.carto.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 CARTO 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 CARTO'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}.carto.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}.carto.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}.carto.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.carto.com/carto-engine/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"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CARTO**
|
||||
|
||||
As described in the [MapConfig File Format](http://docs.carto.com/carto-engine/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 CARTO 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 CARTO Builder and CARTO Engine. It is important to ensure timely processing of queries.
|
||||
|
||||
## Examples
|
||||
|
||||
After instantiating a map from a CARTO 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
141
lib/cartodb/api/auth_api.js
Normal file
141
lib/cartodb/api/auth_api.js
Normal file
@@ -0,0 +1,141 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param metadataBackend
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
* @type {AuthApi}
|
||||
*/
|
||||
function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.mapStore = mapStore;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = AuthApi;
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
AuthApi.prototype.authorizedBySigner = function(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
return callback(null, false); // no signer requested
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param user
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
return callback(null, 0); // no api key, no authorization...
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.metadataBackend.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return val && givenKey === val;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthApi.prototype.authorize = function(req, callback) {
|
||||
var self = this;
|
||||
var user = req.context.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.authorizedByAPIKey(user, req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(req, this);
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, authorized) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! authorized ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
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 AnalysisFilter = require('../models/filter/analysis');
|
||||
|
||||
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 analysisFilter = new AnalysisFilter(filters);
|
||||
var query = analysisFilter.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,75 +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_QueryTables($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;
|
||||
}
|
||||
var qtables = rows[0].cdb_querytables;
|
||||
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, sql, callback) {
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTables($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 and last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
var result = rows[0];
|
||||
|
||||
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 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,20 +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;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
28
lib/cartodb/api/user_limits_api.js
Normal file
28
lib/cartodb/api/user_limits_api.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
*
|
||||
* @param metadataBackend
|
||||
* @param options
|
||||
* @constructor
|
||||
* @type {UserLimitsApi}
|
||||
*/
|
||||
function UserLimitsApi(metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
}
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
|
||||
var self = this;
|
||||
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || self.options.limits.render || 0
|
||||
});
|
||||
});
|
||||
};
|
||||
58
lib/cartodb/backends/analysis-status.js
Normal file
58
lib/cartodb/backends/analysis-status.js
Normal file
@@ -0,0 +1,58 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
|
||||
function AnalysisStatusBackend() {
|
||||
}
|
||||
|
||||
module.exports = AnalysisStatusBackend;
|
||||
|
||||
|
||||
AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
var nodeId = params.nodeId;
|
||||
|
||||
var statusQuery = [
|
||||
'SELECT node_id, status, updated_at, last_error_message as error_message',
|
||||
'FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\''
|
||||
].join(' ');
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
pg.query(statusQuery, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
result = result || {};
|
||||
|
||||
var rows = result.rows || [];
|
||||
|
||||
var statusResponse = rows[0] || {
|
||||
node_id: nodeId,
|
||||
status: 'unknown'
|
||||
};
|
||||
|
||||
if (statusResponse.status !== 'failed') {
|
||||
delete statusResponse.error_message;
|
||||
}
|
||||
|
||||
return callback(null, statusResponse);
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
93
lib/cartodb/backends/analysis.js
Normal file
93
lib/cartodb/backends/analysis.js
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var camshaft = require('camshaft');
|
||||
var fs = require('fs');
|
||||
|
||||
var REDIS_LIMITS = {
|
||||
DB: 5,
|
||||
PREFIX: 'limits:analyses:' // + username
|
||||
};
|
||||
|
||||
function AnalysisBackend (metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
this.setBatchConfig(this.options.batch);
|
||||
this.setLoggerConfig(this.options.logger);
|
||||
}
|
||||
|
||||
module.exports = AnalysisBackend;
|
||||
|
||||
AnalysisBackend.prototype.setBatchConfig = function (options) {
|
||||
var batchConfig = options || {};
|
||||
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;
|
||||
};
|
||||
|
||||
AnalysisBackend.prototype.setLoggerConfig = function (options) {
|
||||
this.loggerConfig = options || {};
|
||||
|
||||
if (this.loggerConfig.filename) {
|
||||
this.stream = fs.createWriteStream(this.loggerConfig.filename, { flags: 'a', encoding: 'utf8' });
|
||||
|
||||
process.on('SIGHUP', function () {
|
||||
if (this.stream) {
|
||||
this.stream.destroy();
|
||||
}
|
||||
|
||||
this.stream = fs.createWriteStream(this.loggerConfig.filename, { flags: 'a', encoding: 'utf8' });
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
analysisConfiguration.logger = {
|
||||
stream: this.stream ? this.stream : process.stdout
|
||||
};
|
||||
|
||||
this.getAnalysesLimits(analysisConfiguration.user, function(err, limits) {
|
||||
analysisConfiguration.limits = limits || {};
|
||||
camshaft.create(analysisConfiguration, analysisDefinition, callback);
|
||||
});
|
||||
};
|
||||
|
||||
AnalysisBackend.prototype.getAnalysesLimits = function(username, callback) {
|
||||
var self = this;
|
||||
|
||||
var analysesLimits = {
|
||||
analyses: {
|
||||
// buffer: {
|
||||
// timeout: 1000,
|
||||
// maxNumberOfRows: 1e6
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(self.options.limits).forEach(function(analysisTypeOrTag) {
|
||||
analysesLimits.analyses[analysisTypeOrTag] = _.extend({}, self.options.limits[analysisTypeOrTag]);
|
||||
});
|
||||
|
||||
var analysesLimitsKey = REDIS_LIMITS.PREFIX + username;
|
||||
this.metadataBackend.redisCmd(REDIS_LIMITS.DB, 'HGETALL', [analysesLimitsKey], function(err, analysesTimeouts) {
|
||||
// analysesTimeouts wil be something like: { moran: 3000, intersection: 5000 }
|
||||
analysesTimeouts = analysesTimeouts || {};
|
||||
|
||||
Object.keys(analysesTimeouts).forEach(function(analysisType) {
|
||||
analysesLimits.analyses[analysisType] = _.defaults(
|
||||
{
|
||||
timeout: Number.isFinite(+analysesTimeouts[analysisType]) ? +analysesTimeouts[analysisType] : 0
|
||||
},
|
||||
analysesLimits.analyses[analysisType]
|
||||
);
|
||||
});
|
||||
|
||||
return callback(null, analysesLimits);
|
||||
});
|
||||
};
|
||||
158
lib/cartodb/backends/dataview.js
Normal file
158
lib/cartodb/backends/dataview.js
Normal file
@@ -0,0 +1,158 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
|
||||
var DataviewFactory = require('../models/dataview/factory');
|
||||
var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory');
|
||||
var OverviewsQueryRewriter = require('../utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function DataviewBackend(analysisBackend) {
|
||||
this.analysisBackend = analysisBackend;
|
||||
}
|
||||
|
||||
module.exports = DataviewBackend;
|
||||
|
||||
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
|
||||
|
||||
var dataviewName = params.dataviewName;
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function runDataviewQuery(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
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 && dataviewDefinition.node.type === 'source') {
|
||||
queryRewriteData = _.extend({}, queryRewriteData, {
|
||||
filters: dataviewDefinition.node.filters,
|
||||
unfiltered_query: dataviewDefinition.sql.own_filter_on
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
if ( queryRewriteData ) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
}
|
||||
|
||||
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
|
||||
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
|
||||
);
|
||||
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
overrides[k] = Number.isFinite(+val) ? +val : val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, overrideParams, this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function runDataviewSearchQuery(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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,4 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
@@ -29,19 +31,21 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
user_params.user_id = user_id;
|
||||
var dbuser = _.template(auth_user, user_params);
|
||||
_.extend(params, {dbuser:dbuser});
|
||||
|
||||
// skip looking up user_password if postgres_auth_pass
|
||||
// doesn't contain the "user_password" label
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) return null;
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
user_params.user_password = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
@@ -81,12 +85,14 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
if (err) throw err;
|
||||
assert.ifError(err);
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) _.extend(params, dbParams);
|
||||
if ( dbParams ) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -94,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
|
||||
}));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
@@ -7,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 = {};
|
||||
@@ -18,15 +25,11 @@ PgQueryRunner.prototype.run = function(username, query, queryHandler, callback)
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
@@ -36,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 || []);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var debug = require('debug')('windshaft:templates');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
@@ -19,9 +21,11 @@ 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();
|
||||
if (!(this instanceof TemplateMaps)) {
|
||||
return new TemplateMaps();
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
@@ -37,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
|
||||
@@ -51,11 +55,9 @@ util.inherits(TemplateMaps, EventEmitter);
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
|
||||
var o = TemplateMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._userTemplateLimit = function() {
|
||||
TemplateMaps.prototype._userTemplateLimit = function() {
|
||||
return this.opts.max_user_templates || 0;
|
||||
};
|
||||
|
||||
@@ -66,7 +68,7 @@ o._userTemplateLimit = function() {
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
TemplateMaps.prototype._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
@@ -76,29 +78,32 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
|
||||
if ( ! _.isUndefined(redisClient) ) {
|
||||
that.redis_pool.release(db, redisClient);
|
||||
}
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
|
||||
var _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
|
||||
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
|
||||
// jshint maxcomplexity:15
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
if ( template.version != '0.0.1' ) {
|
||||
TemplateMaps.prototype._checkInvalidTemplate = function(template) {
|
||||
if ( template.version !== '0.0.1' ) {
|
||||
return new Error("Unsupported template version " + template.version);
|
||||
}
|
||||
var tplname = template.name;
|
||||
if ( ! tplname ) {
|
||||
return new Error("Missing template name");
|
||||
}
|
||||
if ( ! tplname.match(_reValidIdentifier) ) {
|
||||
if ( ! tplname.match(_reValidNameIdentifier) ) {
|
||||
return new Error("Invalid characters in template name '" + tplname + "'");
|
||||
}
|
||||
|
||||
@@ -113,7 +118,7 @@ o._checkInvalidTemplate = function(template) {
|
||||
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
|
||||
var placeholderKey = placeholderKeys[i];
|
||||
|
||||
if (!placeholderKey.match(_reValidIdentifier)) {
|
||||
if (!placeholderKey.match(_reValidPlaceholderIdentifier)) {
|
||||
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
|
||||
}
|
||||
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
|
||||
@@ -130,10 +135,12 @@ o._checkInvalidTemplate = function(template) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(auth.valid_tokens) )
|
||||
if ( ! _.isArray(auth.valid_tokens) ) {
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! auth.valid_tokens.length )
|
||||
}
|
||||
if ( ! auth.valid_tokens.length ) {
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + auth.method);
|
||||
@@ -188,10 +195,10 @@ 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) {
|
||||
TemplateMaps.prototype.addTemplate = function(owner, template, callback) {
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
@@ -213,19 +220,17 @@ o.addTemplate = function(owner, template, callback) {
|
||||
self._redisCmd('HLEN', [ userTemplatesKey ], this);
|
||||
},
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
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);
|
||||
},
|
||||
function validateInstallation(err, wasSet) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if ( ! wasSet ) {
|
||||
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
@@ -251,16 +256,14 @@ o.addTemplate = function(owner, template, callback) {
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delTemplate = function(owner, tpl_id, callback) {
|
||||
TemplateMaps.prototype.delTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function deleteTemplate() {
|
||||
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
|
||||
},
|
||||
function handleDeletion(err, deleted) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if (!deleted) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
@@ -291,8 +294,9 @@ 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) {
|
||||
//
|
||||
TemplateMaps.prototype.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
@@ -305,37 +309,38 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
|
||||
var templateName = template.name;
|
||||
|
||||
if ( tpl_id != templateName ) {
|
||||
if ( tpl_id !== templateName ) {
|
||||
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
|
||||
}
|
||||
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
|
||||
var previousTemplate = null;
|
||||
|
||||
step(
|
||||
function getExistingTemplate() {
|
||||
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
|
||||
},
|
||||
function updateTemplate(err, currentTemplate) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!currentTemplate) {
|
||||
function updateTemplate(err, _currentTemplate) {
|
||||
assert.ifError(err);
|
||||
if (!_currentTemplate) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
previousTemplate = _currentTemplate;
|
||||
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function handleTemplateUpdate(err, didSetNewField) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert.ifError(err);
|
||||
if (didSetNewField) {
|
||||
console.warn('New template created on update operation');
|
||||
debug('New template created on update operation');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
if (self.fingerPrint(JSON.parse(previousTemplate)) !== self.fingerPrint(template)) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, template);
|
||||
@@ -349,8 +354,8 @@ 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) {
|
||||
//
|
||||
TemplateMaps.prototype.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
};
|
||||
|
||||
@@ -364,14 +369,14 @@ o.listTemplates = function(owner, callback) {
|
||||
// @param callback function(err, template)
|
||||
// Return full template definition
|
||||
//
|
||||
o.getTemplate = function(owner, tpl_id, callback) {
|
||||
TemplateMaps.prototype.getTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function getTemplate() {
|
||||
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
assert.ifError(err);
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
@@ -380,7 +385,7 @@ o.getTemplate = function(owner, tpl_id, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
o.isAuthorized = function(template, authTokens) {
|
||||
TemplateMaps.prototype.isAuthorized = function(template, authTokens) {
|
||||
if (!template) {
|
||||
return false;
|
||||
}
|
||||
@@ -416,7 +421,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
|
||||
//
|
||||
@@ -425,14 +430,14 @@ 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]);
|
||||
});
|
||||
return str;
|
||||
}
|
||||
o.instance = function(template, params) {
|
||||
TemplateMaps.prototype.instance = function(template, params) {
|
||||
var all_params = {};
|
||||
var phold = template.placeholders || {};
|
||||
Object.keys(phold).forEach(function(k) {
|
||||
@@ -471,8 +476,16 @@ 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 ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
|
||||
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) {
|
||||
lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
}
|
||||
// Anything else ?
|
||||
}
|
||||
|
||||
@@ -486,7 +499,7 @@ o.instance = function(template, params) {
|
||||
};
|
||||
|
||||
// Return a fingerPrint of the object
|
||||
o.fingerPrint = function(template) {
|
||||
TemplateMaps.prototype.fingerPrint = function(template) {
|
||||
return crypto.createHash('md5')
|
||||
.update(JSON.stringify(template))
|
||||
.digest('hex')
|
||||
110
lib/cartodb/backends/turbo-carto-postgres-datasource.js
Normal file
110
lib/cartodb/backends/turbo-carto-postgres-datasource.js
Normal file
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function createTemplate(method) {
|
||||
return dot.template([
|
||||
'SELECT',
|
||||
'min({{=it._column}}) min_val,',
|
||||
'max({{=it._column}}) max_val,',
|
||||
'avg({{=it._column}}) avg_val,',
|
||||
method,
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
methodTemplates[methodName] = createTemplate(methods[methodName]);
|
||||
return methodTemplates;
|
||||
}, {});
|
||||
|
||||
methodTemplates.category = dot.template([
|
||||
'WITH',
|
||||
'categories AS (',
|
||||
' SELECT {{=it._column}} AS category, count(1) AS value, row_number() OVER (ORDER BY count(1) desc) as rank',
|
||||
' FROM ({{=it._sql}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC, 1 ASC',
|
||||
'),',
|
||||
'agg_categories AS (',
|
||||
' SELECT category',
|
||||
' FROM categories',
|
||||
' WHERE rank <= {{=it._buckets}}',
|
||||
')',
|
||||
'SELECT array_agg(category) AS category FROM agg_categories'
|
||||
].join('\n'));
|
||||
|
||||
var STRATEGY = {
|
||||
SPLIT: 'split',
|
||||
EXACT: 'exact'
|
||||
};
|
||||
|
||||
var method2strategy = {
|
||||
headtails: STRATEGY.SPLIT,
|
||||
category: STRATEGY.EXACT
|
||||
};
|
||||
|
||||
function PostgresDatasource (psql, query) {
|
||||
this.psql = psql;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
PostgresDatasource.prototype.getName = function () {
|
||||
return 'PostgresDatasource';
|
||||
};
|
||||
|
||||
PostgresDatasource.prototype.getRamp = function (column, buckets, method, callback) {
|
||||
if (method && !methodTemplates.hasOwnProperty(method)) {
|
||||
return callback(new Error(
|
||||
'Invalid method "' + method + '", valid methods: [' + Object.keys(methodTemplates).join(',') + ']'
|
||||
));
|
||||
}
|
||||
var methodName = method || 'quantiles';
|
||||
var template = methodTemplates[methodName];
|
||||
|
||||
var query = template({ _column: column, _sql: this.query, _buckets: buckets });
|
||||
|
||||
this.psql.query(query, function (err, resultSet) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var result = getResult(resultSet);
|
||||
var strategy = method2strategy[methodName];
|
||||
var ramp = result[methodName] || [];
|
||||
var stats = {
|
||||
min_val: result.min_val,
|
||||
max_val: result.max_val,
|
||||
avg_val: result.avg_val
|
||||
};
|
||||
// Skip null values from ramp
|
||||
// Generated turbo-carto won't be correct, but better to keep it working than failing
|
||||
// TODO fix cartodb-postgres extension quantification functions
|
||||
ramp = ramp.filter(function(value) { return value !== null; });
|
||||
if (strategy !== STRATEGY.EXACT) {
|
||||
ramp = ramp.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, { ramp: ramp, strategy: strategy, stats: stats });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
function getResult(resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var result = resultSet.rows || [];
|
||||
result = result[0] || {};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = PostgresDatasource;
|
||||
6
lib/cartodb/cache/backend/fastly.js
vendored
6
lib/cartodb/cache/backend/fastly.js
vendored
@@ -1,8 +1,8 @@
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
|
||||
function FastlyCacheBackend(apiKey, serviceId, softPurge) {
|
||||
function FastlyCacheBackend(apiKey, serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: softPurge || true });
|
||||
this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: false });
|
||||
}
|
||||
|
||||
module.exports = FastlyCacheBackend;
|
||||
@@ -14,5 +14,3 @@ module.exports = FastlyCacheBackend;
|
||||
FastlyCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback);
|
||||
};
|
||||
|
||||
module.exports = FastlyCacheBackend;
|
||||
|
||||
2
lib/cartodb/cache/backend/varnish_http.js
vendored
2
lib/cartodb/cache/backend/varnish_http.js
vendored
@@ -28,5 +28,3 @@ VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
24
lib/cartodb/cache/layergroup_affected_tables.js
vendored
Normal file
24
lib/cartodb/cache/layergroup_affected_tables.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
var LruCache = require('lru-cache');
|
||||
|
||||
function LayergroupAffectedTables() {
|
||||
// dbname + layergroupId -> affected tables cache
|
||||
this.cache = new LruCache({ max: 2000 });
|
||||
}
|
||||
|
||||
module.exports = LayergroupAffectedTables;
|
||||
|
||||
LayergroupAffectedTables.prototype.hasAffectedTables = function(dbName, layergroupId) {
|
||||
return this.cache.has(createKey(dbName, layergroupId));
|
||||
};
|
||||
|
||||
LayergroupAffectedTables.prototype.set = function(dbName, layergroupId, affectedTables) {
|
||||
this.cache.set(createKey(dbName, layergroupId), affectedTables);
|
||||
};
|
||||
|
||||
LayergroupAffectedTables.prototype.get = function(dbName, layergroupId) {
|
||||
return this.cache.get(createKey(dbName, layergroupId));
|
||||
};
|
||||
|
||||
function createKey(dbName, layergroupId) {
|
||||
return dbName + ':' + layergroupId;
|
||||
}
|
||||
86
lib/cartodb/cache/named_map_provider_cache.js
vendored
Normal file
86
lib/cartodb/cache/named_map_provider_cache.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
var queue = require('queue-async');
|
||||
|
||||
var LruCache = require("lru-cache");
|
||||
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
|
||||
this.providerCache = new LruCache({ max: 2000 });
|
||||
}
|
||||
|
||||
module.exports = NamedMapProviderCache;
|
||||
|
||||
NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) {
|
||||
var namedMapKey = createNamedMapKey(user, templateId);
|
||||
var namedMapProviders = this.providerCache.get(namedMapKey) || {};
|
||||
|
||||
var providerKey = createProviderKey(config, authToken, params);
|
||||
if (!namedMapProviders.hasOwnProperty(providerKey)) {
|
||||
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.mapConfigAdapter,
|
||||
user,
|
||||
templateId,
|
||||
config,
|
||||
authToken,
|
||||
params
|
||||
);
|
||||
this.providerCache.set(namedMapKey, namedMapProviders);
|
||||
|
||||
// early exit, if provider did not exist we just return it
|
||||
return callback(null, namedMapProviders[providerKey]);
|
||||
}
|
||||
|
||||
var namedMapProvider = namedMapProviders[providerKey];
|
||||
|
||||
var self = this;
|
||||
queue(2)
|
||||
.defer(namedMapProvider.getTemplate.bind(namedMapProvider))
|
||||
.defer(this.templateMaps.getTemplate.bind(this.templateMaps), user, templateId)
|
||||
.awaitAll(function templatesQueueDone(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// We want to reset provider its template has changed
|
||||
// Ideally this should be done in a passive mode where this cache gets notified of template changes
|
||||
var uniqueFingerprints = _.uniq(results.map(self.templateMaps.fingerPrint)).length;
|
||||
if (uniqueFingerprints > 1) {
|
||||
namedMapProvider.reset();
|
||||
}
|
||||
return callback(null, namedMapProvider);
|
||||
});
|
||||
};
|
||||
|
||||
NamedMapProviderCache.prototype.invalidate = function(user, templateId) {
|
||||
this.providerCache.del(createNamedMapKey(user, templateId));
|
||||
};
|
||||
|
||||
function createNamedMapKey(user, templateId) {
|
||||
return user + ':' + templateName(templateId);
|
||||
}
|
||||
|
||||
var providerKey = '{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
var providerKeyTpl = dot.template(providerKey);
|
||||
|
||||
function createProviderKey(config, authToken, params) {
|
||||
var tplValues = _.defaults({}, params, {
|
||||
authToken: authToken || '',
|
||||
configHash: NamedMapMapConfigProvider.configHash(config),
|
||||
layer: '',
|
||||
format: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return providerKeyTpl(tplValues);
|
||||
}
|
||||
14
lib/cartodb/cache/surrogate_keys_cache.js
vendored
14
lib/cartodb/cache/surrogate_keys_cache.js
vendored
@@ -16,9 +16,21 @@ module.exports = SurrogateKeysCache;
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
*/
|
||||
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
|
||||
response.header('Surrogate-Key', cacheObject.key());
|
||||
var newKey = cacheObject.key();
|
||||
response.set('Surrogate-Key', appendSurrogateKey(
|
||||
response.get('Surrogate-Key'),
|
||||
Array.isArray(newKey) ? cacheObject.key().join(' ') : newKey
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
function appendSurrogateKey(currentKey, newKey) {
|
||||
if (!!currentKey) {
|
||||
newKey = currentKey + ' ' + newKey;
|
||||
}
|
||||
return newKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var Windshaft = require('windshaft');
|
||||
var os = require('os');
|
||||
var HealthCheck = require('./monitoring/health_check');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
|
||||
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
if ( ! process.env.PGAPPNAME )
|
||||
process.env.PGAPPNAME='cartodb_tiler';
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
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 redisPool = serverOptions.redis.pool ||
|
||||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
var templateMaps = serverOptions.templateMaps;
|
||||
|
||||
// This is for Templated maps
|
||||
//
|
||||
// "named" is the official, "template" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
|
||||
|
||||
var surrogateKeysCacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
console.warn(logMessage);
|
||||
} else {
|
||||
console.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
// boot
|
||||
var ws = new Windshaft.Server(serverOptions);
|
||||
|
||||
// Override getVersion to include cartodb-specific versions
|
||||
var wsversion = ws.getVersion;
|
||||
ws.getVersion = function() {
|
||||
var version = wsversion();
|
||||
version.windshaft_cartodb = require('../../package.json').version;
|
||||
return version;
|
||||
};
|
||||
|
||||
var ws_sendResponse = ws.sendResponse;
|
||||
// GET routes for which we don't want to request any caching.
|
||||
// POST/PUT/DELETE requests are never cached anyway.
|
||||
var noCacheGETRoutes = [
|
||||
'/',
|
||||
'/version',
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
serverOptions.base_url_mapconfig,
|
||||
serverOptions.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
template_baseurl,
|
||||
template_baseurl + '/:template_id',
|
||||
template_baseurl + '/:template_id/jsonp'
|
||||
];
|
||||
ws.sendResponse = function(res, args) {
|
||||
var that = this;
|
||||
var thatArgs = arguments;
|
||||
var statusCode;
|
||||
if ( res._windshaftStatusCode ) {
|
||||
// Added by our override of sendError
|
||||
statusCode = res._windshaftStatusCode;
|
||||
} else {
|
||||
if ( args.length > 2 ) statusCode = args[2];
|
||||
else {
|
||||
statusCode = args[1] || 200;
|
||||
}
|
||||
}
|
||||
var req = res.req;
|
||||
step (
|
||||
function addCacheChannel() {
|
||||
if ( ! req ) {
|
||||
// having no associated request can happen when
|
||||
// using fake response objects for testing layergroup
|
||||
// creation
|
||||
return false;
|
||||
}
|
||||
if ( ! req.params ) {
|
||||
// service requests (/version, /)
|
||||
// have no need for an X-Cache-Channel
|
||||
return false;
|
||||
}
|
||||
if ( statusCode != 200 ) {
|
||||
// We do not want to cache
|
||||
// unsuccessful responses
|
||||
return false;
|
||||
}
|
||||
if ( _.contains(noCacheGETRoutes, req.route.path) ) {
|
||||
//console.log("Skipping cache channel in route:\n" + req.route.path);
|
||||
return false;
|
||||
}
|
||||
//console.log("Adding cache channel to route\n" + req.route.path + " not matching any in:\n" +
|
||||
// mapCreateRoutes.join("\n"));
|
||||
serverOptions.addCacheChannel(that, req, this);
|
||||
},
|
||||
function sendResponse(err/*, added*/) {
|
||||
if ( err ) console.log(err + err.stack);
|
||||
ws_sendResponse.apply(that, thatArgs);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) console.log(err + err.stack);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var ws_sendError = ws.sendError;
|
||||
ws.sendError = function() {
|
||||
var res = arguments[0];
|
||||
var statusCode = arguments[2];
|
||||
res._windshaftStatusCode = statusCode;
|
||||
ws_sendError.apply(this, arguments);
|
||||
};
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
var NamedMapsController = require('./controllers/named_maps'),
|
||||
namedMapsController = new NamedMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
cartoData,
|
||||
template_baseurl,
|
||||
surrogateKeysCache
|
||||
);
|
||||
namedMapsController.register(ws);
|
||||
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(serverOptions.pgQueryRunner);
|
||||
|
||||
var NamedStaticMapsController = require('./controllers/named_static_maps');
|
||||
var namedStaticMapsController = new NamedStaticMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
ws.staticMapBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi
|
||||
);
|
||||
namedStaticMapsController.register(ws);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive);
|
||||
ws.get('/health', function(req, res) {
|
||||
var healthConfig = global.environment.health || {};
|
||||
|
||||
if (!!healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
healthCheck.check(healthConfig, function(err, result) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime,
|
||||
result: result
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.send(response, ok ? 200 : 503);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.send({enabled: false, ok: true}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
};
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
169
lib/cartodb/controllers/analyses.js
Normal file
169
lib/cartodb/controllers/analyses.js
Normal file
@@ -0,0 +1,169 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var PSQL = require('cartodb-psql');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
function AnalysesController(authApi, pgConnection) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
}
|
||||
|
||||
util.inherits(AnalysesController, BaseController);
|
||||
|
||||
module.exports = AnalysesController;
|
||||
|
||||
AnalysesController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this));
|
||||
};
|
||||
|
||||
AnalysesController.prototype.sendResponse = function(req, res, resource) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
this.send(req, res, resource, 200);
|
||||
};
|
||||
|
||||
AnalysesController.prototype.catalog = function(req, res) {
|
||||
var self = this;
|
||||
var username = req.context.user;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function catalogQuery(err) {
|
||||
assert.ifError(err);
|
||||
var pg = new PSQL(dbParamsFromReqParams(req.params));
|
||||
getMetadata(username, pg, this);
|
||||
},
|
||||
function prepareResponse(err, results) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisIdToTable = results.tables.reduce(function(analysisIdToTable, table) {
|
||||
var analysisId = table.relname.split('_')[2];
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
var catalogWithTables = results.catalog.map(function(analysis) {
|
||||
if (analysisIdToTable.hasOwnProperty(analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
return analysis;
|
||||
});
|
||||
|
||||
return catalogWithTables.sort(function(analysisA, analysisB) {
|
||||
if (!!analysisA.table && !!analysisB.table) {
|
||||
return analysisB.table.size - analysisA.table.size;
|
||||
}
|
||||
|
||||
if (!!analysisA.table) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!!analysisB.table) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
},
|
||||
function sendResponse(err, catalogWithTables) {
|
||||
if (err) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
self.sendError(req, res, err);
|
||||
} else {
|
||||
self.sendResponse(req, res, { catalog: catalogWithTables });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var catalogQueryTpl = dot.template(
|
||||
'SELECT analysis_def->>\'type\' as type, * FROM cartodb.cdb_analysis_catalog WHERE username = \'{{=it._username}}\''
|
||||
);
|
||||
|
||||
var tablesQueryTpl = dot.template([
|
||||
"WITH analysis_tables AS (",
|
||||
" SELECT",
|
||||
" n.nspname AS nspname,",
|
||||
" c.relname AS relname,",
|
||||
" pg_total_relation_size(",
|
||||
" format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))",
|
||||
" ) AS size,",
|
||||
" format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name",
|
||||
" FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n",
|
||||
" WHERE c.relnamespace = n.oid",
|
||||
" AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'",
|
||||
" AND n.nspname IN ('{{=it._username}}', 'public')",
|
||||
")",
|
||||
"SELECT *, pg_size_pretty(size) as size_pretty",
|
||||
"FROM analysis_tables",
|
||||
"ORDER BY size DESC"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function getMetadata(username, pg, callback) {
|
||||
var results = {
|
||||
catalog: [],
|
||||
tables: []
|
||||
};
|
||||
step(
|
||||
function getCatalog() {
|
||||
pg.query(catalogQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleCatalog(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.catalog = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function getTables(err) {
|
||||
assert.ifError(err);
|
||||
pg.query(tablesQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleTables(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.tables = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function finish(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
295
lib/cartodb/controllers/base.js
Normal file
295
lib/cartodb/controllers/base.js
Normal file
@@ -0,0 +1,295 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'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) {
|
||||
this.authApi = authApi;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
BaseController.prototype.req2params = function(req, callback){
|
||||
var self = this;
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
self.req2params(req, callback);
|
||||
} catch (err) {
|
||||
req.profiler.done('req2params');
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = req.context.user;
|
||||
|
||||
if ( req.params.token ) {
|
||||
// Token might match the following patterns:
|
||||
// - {user}@{tpl_id}@{token}:{cache_buster}
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error(
|
||||
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
|
||||
);
|
||||
err.http_status = 403;
|
||||
req.profiler.done('req2params');
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
self.authApi.authorize(req, this);
|
||||
},
|
||||
function validateAuthorization(err, authorized) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) {
|
||||
req.profiler.done('req2params');
|
||||
return callback(err, req);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
req.profiler.done('req2params');
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
if (req.params.dbhost) {
|
||||
res.set('X-Served-By-DB-Host', req.params.dbhost);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
try {
|
||||
// May throw due to dns, see
|
||||
// See http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 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, %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: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
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;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
8
lib/cartodb/controllers/index.js
Normal file
8
lib/cartodb/controllers/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
Analyses: require('./analyses'),
|
||||
Layergroup: require('./layergroup'),
|
||||
Map: require('./map'),
|
||||
NamedMaps: require('./named_maps'),
|
||||
NamedMapsAdmin: require('./named_maps_admin'),
|
||||
ServerInfo: require('./server_info')
|
||||
};
|
||||
429
lib/cartodb/controllers/layergroup.js
Normal file
429
lib/cartodb/controllers/layergroup.js
Normal file
@@ -0,0 +1,429 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var util = require('util');
|
||||
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/provider/map-store-provider');
|
||||
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
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.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
|
||||
module.exports = LayergroupController;
|
||||
|
||||
|
||||
LayergroupController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
||||
this.layer.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
|
||||
this.attributes.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), userMiddleware,
|
||||
this.center.bind(this));
|
||||
|
||||
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/dataview/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
this.analysisNodeStatus.bind(this));
|
||||
};
|
||||
|
||||
LayergroupController.prototype.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.attributes = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveFeatureAttributes(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET ATTRIBUTES');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
};
|
||||
|
||||
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.layer = function(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next();
|
||||
}
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$prepareParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function mapController$getTileOrGrid(err) {
|
||||
assert.ifError(err);
|
||||
self.tileBackend.getTile(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
req.params, this
|
||||
);
|
||||
},
|
||||
function mapController$finalize(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// This function is meant for being called as the very last
|
||||
// step by all endpoints serving tiles or grids
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true
|
||||
};
|
||||
|
||||
var formatStat = 'invalid';
|
||||
if (req.params.format) {
|
||||
var format = req.params.format.replace('.', '_');
|
||||
if (supportedFormats[format]) {
|
||||
formatStat = format;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
if (matches) {
|
||||
errMsg = 'style'+matches[2]+': ' + matches[1];
|
||||
}
|
||||
err.message = errMsg;
|
||||
|
||||
this.sendError(req, res, err, 'TILE RENDER');
|
||||
global.statsClient.increment('windshaft.tiles.error');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
|
||||
} else {
|
||||
this.sendResponse(req, res, tile, 200, headers);
|
||||
global.statsClient.increment('windshaft.tiles.success');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
||||
}
|
||||
};
|
||||
|
||||
LayergroupController.prototype.bbox = function(req, res) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
});
|
||||
};
|
||||
|
||||
LayergroupController.prototype.center = function(req, res) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
});
|
||||
};
|
||||
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.layer = 'all';
|
||||
req.params.format = 'png';
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getImage(err) {
|
||||
assert.ifError(err);
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
format, width, height, zoom, center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_MAP');
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
|
||||
var self = this;
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (req.params.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!!affectedTables) {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
self.send(req, res, body, status, headers);
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
|
||||
|
||||
if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
|
||||
return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
step(
|
||||
function extractSQL() {
|
||||
step(
|
||||
function loadFromStore() {
|
||||
self.mapStore.load(layergroupId, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
step(
|
||||
function getConnection() {
|
||||
self.pgConnection.getConnection(user, this);
|
||||
},
|
||||
function getAffectedTables(err, connection) {
|
||||
assert.ifError(err);
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function buildCacheChannel(err, tables) {
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
|
||||
|
||||
return tables;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
497
lib/cartodb/controllers/map.js
Normal file
497
lib/cartodb/controllers/map.js
Normal file
@@ -0,0 +1,497 @@
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
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');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
|
||||
this.resourcesUrlTemplates = null;
|
||||
if (global.environment.resources_url_templates) {
|
||||
var templates = global.environment.resources_url_templates;
|
||||
|
||||
if (templates.http) {
|
||||
this.resourcesUrlTemplates = this.resourcesUrlTemplates || {};
|
||||
this.resourcesUrlTemplates.http = dot.template(templates.http + '/{{=it.resource}}');
|
||||
}
|
||||
if (templates.https) {
|
||||
this.resourcesUrlTemplates = this.resourcesUrlTemplates || {};
|
||||
this.resourcesUrlTemplates.https = dot.template(templates.https + '/{{=it.resource}}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
|
||||
module.exports = MapController;
|
||||
|
||||
|
||||
MapController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
|
||||
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
|
||||
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
|
||||
app.options(app.base_url_mapconfig, cors('Content-Type'));
|
||||
};
|
||||
|
||||
MapController.prototype.createGet = function(req, res){
|
||||
req.profiler.start('windshaft.createmap_get');
|
||||
|
||||
this.create(req, res, function createGet$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if ( ! req.params.config ) {
|
||||
throw new Error('layergroup GET needs a "config" parameter');
|
||||
}
|
||||
return JSON.parse(req.params.config);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.createPost = function(req, res) {
|
||||
req.profiler.start('windshaft.createmap_post');
|
||||
|
||||
this.create(req, res, function createPost$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error('layergroup POST data must be of type application/json');
|
||||
}
|
||||
return req.body;
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.instantiate = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
|
||||
if (!req.is('application/json')) {
|
||||
return callback(new Error('Template POST data must be of type application/json'));
|
||||
}
|
||||
return callback(null, req.body);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.jsonp = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
|
||||
var err = null;
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
err = new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
err = new Error('Invalid config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return callback(err, templateParams);
|
||||
});
|
||||
};
|
||||
|
||||
MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
var self = this;
|
||||
|
||||
var mapConfig;
|
||||
|
||||
var context = {};
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: req.context.user,
|
||||
db: {
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: req.context.user,
|
||||
apiKey: req.params.api_key
|
||||
}
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
|
||||
},
|
||||
function createLayergroup(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, req.params,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
if (err) {
|
||||
if (Number.isFinite(err.layerIndex)) {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = err.http_status;
|
||||
|
||||
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
|
||||
error.http_status = 400;
|
||||
}
|
||||
|
||||
error.type = 'layer';
|
||||
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(err.layerIndex),
|
||||
index: err.layerIndex,
|
||||
type: mapConfig.layerType(err.layerIndex)
|
||||
};
|
||||
|
||||
err = error;
|
||||
}
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
var analysesResults = context.analysesResults || [];
|
||||
self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
|
||||
self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
|
||||
addContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getTemplateParams() {
|
||||
prepareParamsFn(this);
|
||||
},
|
||||
function getTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.metadataBackend,
|
||||
self.userLimitsApi,
|
||||
self.mapConfigAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
req.query.auth_token,
|
||||
req.params
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, rendererParams,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, mapConfigProvider.analysesResults, this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
|
||||
} else {
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
var _mapConfig = mapConfig.obj();
|
||||
self.addDataviewsAndWidgetsUrls(cdbuser, layergroup, _mapConfig);
|
||||
self.addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
|
||||
addContextMetadata(layergroup, _mapConfig, mapConfigProvider.context);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err, layergroup);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asynchronously
|
||||
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
}
|
||||
if ( err ) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = mapconfig.getLayers().map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
|
||||
step(
|
||||
function getPgConnection() {
|
||||
self.pgConnection.getConnection(username, this);
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result);
|
||||
|
||||
var lastUpdateTime = result.getLastUpdatedAt();
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
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', result.getCacheChannel());
|
||||
if (result.tables && result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
return analysesResults.reduce(function(lastUpdateTime, analysis) {
|
||||
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
|
||||
var nodeUpdatedAtDate = node.getUpdatedAt();
|
||||
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
|
||||
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
|
||||
}, lastUpdateTime);
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce(function(nodesIdMap, node) {
|
||||
if (node.params.id) {
|
||||
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
|
||||
var nodeRepr = {
|
||||
status: node.getStatus(),
|
||||
url: this.getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodeRepr.query = node.getQuery();
|
||||
}
|
||||
if (node.getStatus() === 'failed') {
|
||||
nodeRepr.error_message = node.getErrorMessage();
|
||||
}
|
||||
nodesIdMap[node.params.id] = nodeRepr;
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}.bind(this), {})
|
||||
});
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
|
||||
this.addDataviewsUrls(username, layergroup, mapConfig);
|
||||
this.addWidgetsUrl(username, layergroup, mapConfig);
|
||||
};
|
||||
|
||||
MapController.prototype.addDataviewsUrls = function(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: this.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
|
||||
layer.widgets = layer.widgets || {};
|
||||
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: this.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
}
|
||||
return layer;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
MapController.prototype.getUrls = function(username, resource) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
MapController.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var urls = {};
|
||||
var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url || {};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnUrl.http,
|
||||
user: username,
|
||||
port: global.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
}
|
||||
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnUrl.https,
|
||||
user: username,
|
||||
port: global.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
@@ -1,372 +1,353 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
function NamedMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsController, BaseController);
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this));
|
||||
app.post(this.templateBaseUrl, this.create.bind(this));
|
||||
app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this));
|
||||
app.get(this.templateBaseUrl + '/:template_id', this.retrieve.bind(this));
|
||||
app.del(this.templateBaseUrl + '/:template_id', this.destroy.bind(this));
|
||||
app.get(this.templateBaseUrl, this.list.bind(this));
|
||||
app.options(this.templateBaseUrl + '/:template_id', this.options.bind(this));
|
||||
app.post(this.templateBaseUrl + '/:template_id', this.instantiate.bind(this));
|
||||
app.get(app.base_url_templated +
|
||||
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
|
||||
this.staticMap.bind(this));
|
||||
};
|
||||
|
||||
// Add a template
|
||||
NamedMapsController.prototype.create = function(req, res) {
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'POST TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
NamedMapsController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'PUT TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Get a specific template
|
||||
NamedMapsController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
function sendResponse(err, result) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.log('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE')
|
||||
);
|
||||
};
|
||||
if (!result || !!result.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Delete a specific template
|
||||
NamedMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(result.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(result.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
finishFn(self.app, res, 'DELETE TEMPLATE', ['', 204])
|
||||
);
|
||||
};
|
||||
|
||||
// Get a list of owned templates
|
||||
NamedMapsController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE LIST')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.instantiate = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
step(
|
||||
function() {
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
|
||||
self.instantiateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.options = function(req, res, next) {
|
||||
this.app.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instantiate a template with a json call.
|
||||
* callback query argument is mandatory
|
||||
*/
|
||||
NamedMapsController.prototype.jsonp = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
step(
|
||||
function() {
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
throw new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
var config = {};
|
||||
if(req.query.config) {
|
||||
try {
|
||||
config = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
throw new Error('badformed config parameter, should be a valid JSON');
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
self.instantiateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
self.send(req, res, resource, 200);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
|
||||
NamedMapsController.prototype.tile = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
var cdbUser = req.context.user;
|
||||
|
||||
var template;
|
||||
var layergroup;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = templateName(req.params.template_id);
|
||||
var auth_token = req.query.auth_token;
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function getTemplate(){
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function checkAuthorized(err, templateValue) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! templateValue ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template = templateValue;
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(template, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
this
|
||||
);
|
||||
},
|
||||
function getTile(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.tileBackend.getTile(namedMapProvider, req.params, this);
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByCert');
|
||||
req.profiler.add(stats);
|
||||
}
|
||||
|
||||
return self.templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.setDBParams(cdbuser, fakereq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
if ( req.profiler ) req.profiler.done('setDBParams');
|
||||
if ( err ) throw err;
|
||||
self.metadataBackend.getUserMapKey(cdbuser, this);
|
||||
},
|
||||
function createLayergroup(err, val) {
|
||||
if ( req.profiler ) req.profiler.done('getUserMapKey');
|
||||
if ( err ) throw err;
|
||||
fakereq.params.api_key = val;
|
||||
self.app.createLayergroup(layergroup, fakereq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED_MAP_TILE');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
return layergroup;
|
||||
},
|
||||
callback
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.finish_instantiation = function(err, response, res) {
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.format = 'png';
|
||||
req.params.layer = 'all';
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
this
|
||||
);
|
||||
},
|
||||
function prepareLayerVisibility(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function incrementMapViews(err, image, headers, stats) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
|
||||
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
|
||||
if (err) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
|
||||
}
|
||||
next(err, image, headers, stats);
|
||||
});
|
||||
});
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
}
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach(function (layer, index) {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// overwrites 'all' default filter
|
||||
req.params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
user,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function finishFn(app, res, description, okResponse) {
|
||||
return function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err) {
|
||||
statusCode = 400;
|
||||
response = { error: '' + err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
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
|
||||
}
|
||||
app.sendError(res, response, statusCode, description, err);
|
||||
} else {
|
||||
app.sendResponse(res, okResponse || [response, statusCode]);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
function handleTemplateView(err, template) {
|
||||
assert.ifError(err);
|
||||
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+params.zoom)) {
|
||||
zoomCenter.zoom = +params.zoom;
|
||||
}
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
function estimateBoundsIfNoImageOpts(err, imageOpts) {
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
|
||||
if (err) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
|
||||
return next(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
function returnCallback(err, imageOpts) {
|
||||
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (authenticated !== 1) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' ) {
|
||||
throw new Error(description);
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return Number.isFinite(view.bounds[prop]);
|
||||
});
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
203
lib/cartodb/controllers/named_maps_admin.js
Normal file
203
lib/cartodb/controllers/named_maps_admin.js
Normal file
@@ -0,0 +1,203 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.authApi = authApi;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsAdminController, BaseController);
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function(app) {
|
||||
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
|
||||
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
|
||||
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
|
||||
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
|
||||
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST')
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status) {
|
||||
return function finish(err, response){
|
||||
if (err) {
|
||||
controller.sendError(req, res, err, description);
|
||||
} else {
|
||||
controller.send(req, res, response, status || 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var _ = require('underscore');
|
||||
|
||||
function NamedStaticMapsController(app, serverOptions, templateMaps, staticMapBackend, surrogateKeysCache,
|
||||
tablesExtentApi) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.staticMapBackend = staticMapBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
}
|
||||
|
||||
module.exports = NamedStaticMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedStaticMapsController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', this.named.bind(this));
|
||||
};
|
||||
|
||||
NamedStaticMapsController.prototype.named = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
var cdbUser = cdbRequest.userByReq(req);
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
var template;
|
||||
var layergroupConfig;
|
||||
var layergroupId;
|
||||
var fakeReq;
|
||||
var cacheChannel;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.app.req2params(req, this);
|
||||
},
|
||||
function getTemplate(err) {
|
||||
assert.ifError(err);
|
||||
self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this);
|
||||
},
|
||||
function checkExists(err, tpl) {
|
||||
assert.ifError(err);
|
||||
if (!tpl) {
|
||||
var notFoundErr = new Error(
|
||||
"Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found"
|
||||
);
|
||||
notFoundErr.http_status = 404;
|
||||
throw notFoundErr;
|
||||
}
|
||||
return tpl;
|
||||
},
|
||||
function checkAuthorized(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
var authorizationFailedErr = new Error('Failed to authorize template');
|
||||
authorizationFailedErr.http_status = 403;
|
||||
throw authorizationFailedErr;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
var unauthorizedErr = new Error('Unauthorized template instantiation');
|
||||
unauthorizedErr.http_status = 403;
|
||||
throw unauthorizedErr;
|
||||
}
|
||||
|
||||
return tpl;
|
||||
},
|
||||
function prepareParams(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
template = tpl;
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch (e) {
|
||||
throw new Error('malformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return templateParams;
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
layergroupConfig = layergroup;
|
||||
fakeReq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.setDBParams(cdbUser, fakeReq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
assert.ifError(err);
|
||||
self.app.createLayergroup(layergroupConfig, fakeReq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
|
||||
// added by createLayergroup
|
||||
cacheChannel = res.header('X-Cache-Channel');
|
||||
res.removeHeader('X-Cache-Channel');
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name));
|
||||
|
||||
layergroupId = layergroup.layergroupid.split(":")[0];
|
||||
|
||||
return null;
|
||||
},
|
||||
function staticImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
getStaticImageOptions(template, this);
|
||||
},
|
||||
function estimateBounds(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
var defaultZoomCenter = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
var dbTables = cacheChannel.split(':');
|
||||
if (dbTables.length <= 1 || dbTables[1].length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var tableNames = dbTables[1].split(',');
|
||||
if (tableNames.length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) {
|
||||
next(null, result || defaultZoomCenter);
|
||||
});
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var staticImageReq = {
|
||||
headers: _.clone(fakeReq.headers),
|
||||
params: _.extend(_.clone(fakeReq.params), {
|
||||
token: layergroupId,
|
||||
format: req.params.format
|
||||
})
|
||||
};
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (!err.error) {
|
||||
err.error = err.message;
|
||||
}
|
||||
self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err);
|
||||
} else {
|
||||
res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
self.app.sendResponse(res, [image, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStaticImageOptions(template, callback) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
return callback(null, zoomCenter);
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return callback(null, bounds);
|
||||
}
|
||||
}
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return !!view.bounds[prop];
|
||||
});
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
48
lib/cartodb/controllers/server_info.js
Normal file
48
lib/cartodb/controllers/server_info.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var HealthCheck = require('../monitoring/health_check');
|
||||
|
||||
var WELCOME_MSG = "This is the CartoDB Maps API, " +
|
||||
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
|
||||
|
||||
|
||||
function ServerInfoController(versions) {
|
||||
this.healthConfig = global.environment.health || {};
|
||||
this.healthCheck = new HealthCheck(global.environment.disabled_file);
|
||||
this.versions = versions || {};
|
||||
}
|
||||
|
||||
module.exports = ServerInfoController;
|
||||
|
||||
ServerInfoController.prototype.register = function(app) {
|
||||
app.get('/health', this.health.bind(this));
|
||||
app.get('/', this.welcome.bind(this));
|
||||
app.get('/version', this.version.bind(this));
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.welcome = function(req, res) {
|
||||
res.status(200).send(WELCOME_MSG);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.version = function(req, res) {
|
||||
res.status(200).send(this.versions);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.health = function(req, res) {
|
||||
if (!!this.healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
this.healthCheck.check(function(err) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.status(ok ? 200 : 503).send(response);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.status(200).send({enabled: false, ok: true});
|
||||
}
|
||||
};
|
||||
11
lib/cartodb/middleware/cors.js
Normal file
11
lib/cartodb/middleware/cors.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = function cors(extraHeaders) {
|
||||
return function(req, res, next) {
|
||||
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
|
||||
if(extraHeaders) {
|
||||
baseHeaders += ", " + extraHeaders;
|
||||
}
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.set("Access-Control-Allow-Headers", baseHeaders);
|
||||
next();
|
||||
};
|
||||
};
|
||||
7
lib/cartodb/middleware/user.js
Normal file
7
lib/cartodb/middleware/user.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
req.context.user = cdbRequest.userByReq(req);
|
||||
next();
|
||||
};
|
||||
@@ -8,18 +8,17 @@ module.exports = CdbRequest;
|
||||
|
||||
|
||||
CdbRequest.prototype.userByReq = function(req) {
|
||||
var host = req.headers.host;
|
||||
var host = req.headers.host || '';
|
||||
if (req.params.user) {
|
||||
return req.params.user;
|
||||
}
|
||||
var mat = host.match(this.RE_USER_FROM_HOST);
|
||||
if ( ! mat ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
|
||||
global.logger.error("Pattern '%s' does not match hostname '%s'", this.RE_USER_FROM_HOST, host);
|
||||
return;
|
||||
}
|
||||
// console.log("Matches: "); console.dir(mat);
|
||||
if ( mat.length !== 2 ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' gave unexpected matches against '" + host + "': ", mat);
|
||||
global.logger.error("Pattern '%s' gave unexpected matches against '%s': %s", this.RE_USER_FROM_HOST, host, mat);
|
||||
return;
|
||||
}
|
||||
return mat[1];
|
||||
|
||||
282
lib/cartodb/models/dataview/aggregation.js
Normal file
282
lib/cartodb/models/dataview/aggregation.js
Normal file
@@ -0,0 +1,282 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:aggregation');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' count(1) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
count: [],
|
||||
sum: ['aggregationColumn'],
|
||||
avg: ['aggregationColumn'],
|
||||
min: ['aggregationColumn'],
|
||||
max: ['aggregationColumn']
|
||||
};
|
||||
|
||||
var TYPE = 'aggregation';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'count' // it could be, e.g., sum if column is numeric
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Aggregation(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Aggregation expects `column` in widget options');
|
||||
}
|
||||
|
||||
if (!_.isString(options.aggregation)) {
|
||||
throw new Error('Aggregation expects `aggregation` operation in widget options');
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
|
||||
}
|
||||
|
||||
var requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
var missingOptions = _.difference(requiredOptions, Object.keys(options));
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = new BaseWidget();
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
|
||||
Aggregation.prototype.format = function(result) {
|
||||
var categories = [];
|
||||
var count = 0;
|
||||
var nulls = 0;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var categoriesCount = 0;
|
||||
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
count = firstRow.count;
|
||||
nulls = firstRow.nulls_count;
|
||||
minValue = firstRow.min_val;
|
||||
maxValue = firstRow.max_val;
|
||||
categoriesCount = firstRow.categories_count;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val', 'max_val', 'categories_count'));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
categoriesCount: categoriesCount,
|
||||
categories: categories
|
||||
};
|
||||
};
|
||||
|
||||
var filterCategoriesQueryTpl = dot.template([
|
||||
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_search',
|
||||
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
|
||||
'GROUP BY {{=it._column}}'
|
||||
].join('\n'));
|
||||
|
||||
var searchQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'search_unfiltered AS (',
|
||||
' {{=it._searchUnfiltered}}',
|
||||
'),',
|
||||
'search_filtered AS (',
|
||||
' {{=it._searchFiltered}}',
|
||||
'),',
|
||||
'search_union AS (',
|
||||
' SELECT * FROM search_unfiltered',
|
||||
' UNION ALL',
|
||||
' SELECT * FROM search_filtered',
|
||||
')',
|
||||
'SELECT category, sum(value) AS value',
|
||||
'FROM search_union',
|
||||
'GROUP BY category',
|
||||
'ORDER BY value desc'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
var self = this;
|
||||
|
||||
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
var query = searchQueryTpl({
|
||||
_searchUnfiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: '0',
|
||||
_userQuery: _userQuery
|
||||
}),
|
||||
_searchFiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: 'count(1)',
|
||||
_userQuery: _userQuery
|
||||
})
|
||||
});
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, {type: self.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
Aggregation.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Aggregation.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
};
|
||||
26
lib/cartodb/models/dataview/base.js
Normal file
26
lib/cartodb/models/dataview/base.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function BaseDataview() {}
|
||||
|
||||
module.exports = BaseDataview;
|
||||
|
||||
BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
var self = this;
|
||||
this.sql(psql, override, function(err, query) {
|
||||
psql.query(query, function(err, result) {
|
||||
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
result = self.format(result, override);
|
||||
result.type = self.getType();
|
||||
|
||||
return callback(null, result);
|
||||
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BaseDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
};
|
||||
18
lib/cartodb/models/dataview/factory.js
Normal file
18
lib/cartodb/models/dataview/factory.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var dataviews = require('./');
|
||||
|
||||
var DataviewFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
|
||||
getDataview: function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
if (!this.dataviews[type]) {
|
||||
throw new Error('Invalid dataview type: "' + type + '"');
|
||||
}
|
||||
return new this.dataviews[type](query, dataviewDefinition.options);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataviewFactory;
|
||||
104
lib/cartodb/models/dataview/formula.js
Normal file
104
lib/cartodb/models/dataview/formula.js
Normal file
@@ -0,0 +1,104 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:formula');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
'{{=it._operation}}({{=it._column}}) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n'));
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
count: true,
|
||||
avg: true,
|
||||
sum: true,
|
||||
min: true,
|
||||
max: true
|
||||
};
|
||||
|
||||
var TYPE = 'formula';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'name',
|
||||
operation: 'count' // count, sum, avg
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Formula(query, options) {
|
||||
if (!_.isString(options.operation)) {
|
||||
throw new Error('Formula expects `operation` in widget options');
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error("Formula does not support '" + options.operation + "' operation");
|
||||
}
|
||||
|
||||
if (options.operation !== 'count' && !_.isString(options.column)) {
|
||||
throw new Error('Formula expects `column` in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: _query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
Formula.prototype.format = function(result) {
|
||||
var formattedResult = {
|
||||
operation: this.operation,
|
||||
result: 0,
|
||||
nulls: 0
|
||||
};
|
||||
|
||||
if (result.rows.length) {
|
||||
formattedResult.operation = this.operation;
|
||||
formattedResult.result = result.rows[0].result;
|
||||
formattedResult.nulls = result.rows[0].nulls_count;
|
||||
}
|
||||
|
||||
return formattedResult;
|
||||
};
|
||||
|
||||
Formula.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Formula.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
};
|
||||
307
lib/cartodb/models/dataview/histogram.js
Normal file
307
lib/cartodb/models/dataview/histogram.js
Normal file
@@ -0,0 +1,307 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:dataview:histogram');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' avg({{=it._column}})::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
var TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name',
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Histogram(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
Histogram.prototype = new BaseWidget();
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.query
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
debug('overriding with %j', override);
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override)
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype.format = function(result, override) {
|
||||
override = override || {};
|
||||
var buckets = [];
|
||||
|
||||
var binsCount = getBinsCount(override);
|
||||
var width = getWidth(override);
|
||||
var binsStart = getBinStart(override);
|
||||
var nulls = 0;
|
||||
var avg;
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
binsCount = firstRow.bins_number;
|
||||
width = firstRow.bin_width || width;
|
||||
avg = firstRow.avg_val;
|
||||
nulls = firstRow.nulls_count;
|
||||
binsStart = override.hasOwnProperty('start') ? getBinStart(override) : firstRow.min;
|
||||
|
||||
buckets = result.rows.map(function(row) {
|
||||
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: width,
|
||||
bins_count: binsCount,
|
||||
bins_start: binsStart,
|
||||
nulls: nulls,
|
||||
avg: avg,
|
||||
bins: buckets
|
||||
};
|
||||
};
|
||||
|
||||
function getBinStart(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
function getBinEnd(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
function getBinsCount(override) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
|
||||
function getWidth(override) {
|
||||
var width = 0;
|
||||
var binsCount = override.bins;
|
||||
|
||||
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
|
||||
width = (override.end - override.start) / binsCount;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
Histogram.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Histogram.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
};
|
||||
6
lib/cartodb/models/dataview/index.js
Normal file
6
lib/cartodb/models/dataview/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
66
lib/cartodb/models/dataview/list.js
Normal file
66
lib/cartodb/models/dataview/list.js
Normal file
@@ -0,0 +1,66 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var BaseWidget = require('./base');
|
||||
|
||||
var TYPE = 'list';
|
||||
|
||||
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name', 'description']
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function List(query, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!Array.isArray(options.columns)) {
|
||||
throw new Error('List expects `columns` array in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.columns = options.columns;
|
||||
}
|
||||
|
||||
List.prototype = new BaseWidget();
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
|
||||
List.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
}
|
||||
|
||||
var listSql = listSqlTpl({
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
|
||||
return callback(null, listSql);
|
||||
};
|
||||
|
||||
List.prototype.format = function(result) {
|
||||
return {
|
||||
rows: result.rows
|
||||
};
|
||||
};
|
||||
|
||||
List.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
List.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
};
|
||||
141
lib/cartodb/models/dataview/overviews/aggregation.js
Normal file
141
lib/cartodb/models/dataview/overviews/aggregation.js
Normal file
@@ -0,0 +1,141 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../aggregation');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' sum(_feature_count) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, filters, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = {
|
||||
count: dot.template('sum(_feature_count)'),
|
||||
sum: dot.template('sum({{=it._aggregationColumn}}*_feature_count)')
|
||||
};
|
||||
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl[this.aggregation]({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
88
lib/cartodb/models/dataview/overviews/base.js
Normal file
88
lib/cartodb/models/dataview/overviews/base.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var _ = require('underscore');
|
||||
var BaseDataview = require('../base');
|
||||
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options) {
|
||||
this.BaseDataview = BaseDataview;
|
||||
this.query = query;
|
||||
this.queryOptions = queryOptions;
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions);
|
||||
}
|
||||
|
||||
module.exports = BaseOverviewsDataview;
|
||||
|
||||
BaseOverviewsDataview.prototype = new BaseDataview();
|
||||
BaseOverviewsDataview.prototype.constructor = BaseOverviewsDataview;
|
||||
|
||||
// TODO: parameterized these settings
|
||||
var SETTINGS = {
|
||||
// use overviews as a default fallback strategy
|
||||
defaultOverviews: false,
|
||||
|
||||
// minimum ratio of bounding box size to grid size
|
||||
// (this would ideally be based on the viewport size in pixels)
|
||||
zoomLevelFactor: 1024.0
|
||||
};
|
||||
|
||||
// Compute zoom level so that the the resolution grid size of the
|
||||
// selected overview is smaller (zoomLevelFactor times smaller at least)
|
||||
// than the bounding box size.
|
||||
BaseOverviewsDataview.prototype.zoomLevelForBbox = function(bbox) {
|
||||
var pxPerTile = 256.0;
|
||||
var earthWidth = 360.0;
|
||||
// TODO: now we assume overviews are computed for 1-pixel tolerance;
|
||||
// should use extended overviews metadata to compute this properly.
|
||||
if ( bbox ) {
|
||||
var bboxValues = _.map(bbox.split(','), function(v) { return +v; });
|
||||
var w = Math.abs(bboxValues[2]-bboxValues[0]);
|
||||
var h = Math.abs(bboxValues[3]-bboxValues[1]);
|
||||
var maxDim = Math.min(w, h);
|
||||
|
||||
// Find minimum suitable z
|
||||
// note that the QueryRewirter will use the minimum level overview
|
||||
// of level >= z if it exists, and otherwise the base table
|
||||
var z = Math.ceil(-Math.log(maxDim*pxPerTile/earthWidth/SETTINGS.zoomLevelFactor)/Math.log(2.0));
|
||||
return Math.max(z, 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.rewrittenQuery = function(query) {
|
||||
var zoom_level = this.zoomLevelForBbox(this.options.bbox);
|
||||
return this.queryRewriter.query(query, this.queryRewriteData, { zoom_level: zoom_level });
|
||||
};
|
||||
|
||||
// Default behaviour
|
||||
BaseOverviewsDataview.prototype.defaultSql = function(psql, filters, override, callback) {
|
||||
var query = this.query;
|
||||
var dataview = this.baseDataview;
|
||||
if ( SETTINGS.defaultOverviews ) {
|
||||
query = this.rewrittenQuery(query);
|
||||
dataview = new this.BaseDataview(query, this.queryOptions);
|
||||
}
|
||||
return dataview.sql(psql, filters, override, callback);
|
||||
};
|
||||
|
||||
// default implementation that can be override in derived classes:
|
||||
|
||||
BaseOverviewsDataview.prototype.sql = function(psql, filters, override, callback) {
|
||||
return this.defaultSql(psql, filters, override, callback);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return this.baseDataview.search(psql, userQuery, callback);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.format = function(result) {
|
||||
return this.baseDataview.format(result);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.getType = function() {
|
||||
return this.baseDataview.getType();
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.toString = function() {
|
||||
return this.baseDataview.toString();
|
||||
};
|
||||
32
lib/cartodb/models/dataview/overviews/factory.js
Normal file
32
lib/cartodb/models/dataview/overviews/factory.js
Normal file
@@ -0,0 +1,32 @@
|
||||
var parentFactory = require('../factory');
|
||||
var dataviews = require('./');
|
||||
|
||||
function OverviewsDataviewFactory(queryRewriter, queryRewriteData, options) {
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
OverviewsDataviewFactory.prototype.getDataview = function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
var dataviews = OverviewsDataviewMetaFactory.dataviews;
|
||||
if ( !this.queryRewriter || !this.queryRewriteData || !dataviews[type] ) {
|
||||
return parentFactory.getDataview(query, dataviewDefinition);
|
||||
}
|
||||
return new dataviews[type](
|
||||
query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options
|
||||
);
|
||||
};
|
||||
|
||||
var OverviewsDataviewMetaFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
|
||||
getFactory: function(queryRewriter, queryRewriteData, options) {
|
||||
return new OverviewsDataviewFactory(queryRewriter, queryRewriteData, options);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = OverviewsDataviewMetaFactory;
|
||||
56
lib/cartodb/models/dataview/overviews/formula.js
Normal file
56
lib/cartodb/models/dataview/overviews/formula.js
Normal file
@@ -0,0 +1,56 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
};
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, filters, override, callback) {
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if ( formulaQueryTpl ) {
|
||||
// supported formula for use with overviews
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
callback = callback || override;
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, filters, override, callback);
|
||||
};
|
||||
217
lib/cartodb/models/dataview/overviews/histogram.js
Normal file
217
lib/cartodb/models/dataview/overviews/histogram.js
Normal file
@@ -0,0 +1,217 @@
|
||||
var _ = require('underscore');
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../histogram');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count)::numeric AS avg,',
|
||||
' sum(_feature_count) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
function Histogram(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
Histogram.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.rewrittenQuery(this.query)
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: override.start,
|
||||
_end: override.end
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
6
lib/cartodb/models/dataview/overviews/index.js
Normal file
6
lib/cartodb/models/dataview/overviews/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
@@ -0,0 +1,11 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../list');
|
||||
|
||||
function List(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
}
|
||||
|
||||
List.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
35
lib/cartodb/models/filter/analysis.js
Normal file
35
lib/cartodb/models/filter/analysis.js
Normal file
@@ -0,0 +1,35 @@
|
||||
var filters = {
|
||||
category: require('./analysis/category'),
|
||||
range: require('./analysis/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 AnalysisFilters(filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
AnalysisFilters.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 = AnalysisFilters;
|
||||
79
lib/cartodb/models/filter/analysis/category.js
Normal file
79
lib/cartodb/models/filter/analysis/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}}) _analysis_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/analysis/range.js
Normal file
43
lib/cartodb/models/filter/analysis/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}}) _analysis_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
|
||||
});
|
||||
};
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,333 @@
|
||||
var queue = require('queue-async');
|
||||
var debug = require('debug')('windshaft:analysis');
|
||||
|
||||
var camshaft = require('camshaft');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function AnalysisMapConfigAdapter(analysisBackend) {
|
||||
this.analysisBackend = analysisBackend;
|
||||
}
|
||||
|
||||
module.exports = AnalysisMapConfigAdapter;
|
||||
|
||||
AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
// jshint maxcomplexity:7
|
||||
var self = this;
|
||||
|
||||
if (!shouldAdaptLayers(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var analysisConfiguration = context.analysisConfiguration;
|
||||
|
||||
var filters = {};
|
||||
if (params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
var dataviewsFilters = filters.dataviews || {};
|
||||
debug(dataviewsFilters);
|
||||
var dataviews = requestMapConfig.dataviews || {};
|
||||
|
||||
var errors = getDataviewsErrors(dataviews);
|
||||
if (errors.length > 0) {
|
||||
return callback(errors);
|
||||
}
|
||||
|
||||
var dataviewsFiltersBySourceId = Object.keys(dataviewsFilters).reduce(function(bySourceId, dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
if (dataview) {
|
||||
var sourceId = dataview.source.id;
|
||||
if (!bySourceId.hasOwnProperty(sourceId)) {
|
||||
bySourceId[sourceId] = {};
|
||||
}
|
||||
|
||||
bySourceId[sourceId][dataviewName] = getFilter(dataview, dataviewsFilters[dataviewName]);
|
||||
}
|
||||
return bySourceId;
|
||||
}, {});
|
||||
|
||||
debug(dataviewsFiltersBySourceId);
|
||||
|
||||
debug('mapconfig input', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||
|
||||
function createAnalysis(analysisDefinition, done) {
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
|
||||
if (err) {
|
||||
var error = new Error(err.message);
|
||||
error.type = 'analysis';
|
||||
error.analysis = {
|
||||
id: analysisDefinition.id,
|
||||
node_id: err.node_id,
|
||||
type: analysisDefinition.type
|
||||
};
|
||||
return done(error);
|
||||
}
|
||||
|
||||
done(null, analysis);
|
||||
});
|
||||
}
|
||||
|
||||
var analysesQueue = queue(1);
|
||||
requestMapConfig.analyses.forEach(function(analysis) {
|
||||
analysesQueue.defer(createAnalysis, analysis);
|
||||
});
|
||||
|
||||
analysesQueue.awaitAll(function(err, analysesResults) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = analysesResults.reduce(function(sourceId2Query, analysis) {
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Query[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Query[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
return sourceId2Query;
|
||||
}, {});
|
||||
|
||||
var missingNodesErrors = [];
|
||||
|
||||
requestMapConfig.layers = requestMapConfig.layers.map(function(layer, layerIndex) {
|
||||
if (getLayerSourceId(layer)) {
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
var layerNode = sourceId2Node[layerSourceId];
|
||||
if (layerNode) {
|
||||
var analysisSql = layerQuery(layerNode);
|
||||
var sqlQueryWrap = layer.options.sql_wrap;
|
||||
if (sqlQueryWrap) {
|
||||
layer.options.sql_raw = analysisSql;
|
||||
analysisSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, analysisSql);
|
||||
}
|
||||
layer.options.sql = analysisSql;
|
||||
layer.options.columns = getDataviewsColumns(getLayerDataviews(layer, dataviews));
|
||||
} else {
|
||||
missingNodesErrors.push(
|
||||
new Error('Missing analysis node.id="' + layerSourceId +'" for layer='+layerIndex)
|
||||
);
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
|
||||
var missingDataviewsNodesErrors = getMissingDataviewsSourceIds(dataviews, sourceId2Node);
|
||||
if (missingNodesErrors.length > 0 || missingDataviewsNodesErrors.length > 0) {
|
||||
return callback(missingNodesErrors.concat(missingDataviewsNodesErrors));
|
||||
}
|
||||
|
||||
// Augment dataviews with sql from analyses
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = requestMapConfig.dataviews[dataviewName];
|
||||
var dataviewSourceId = dataview.source.id;
|
||||
var dataviewNode = sourceId2Node[dataviewSourceId];
|
||||
dataview.node = {
|
||||
type: dataviewNode.type,
|
||||
filters: dataviewNode.getFilters()
|
||||
};
|
||||
dataview.sql = {
|
||||
own_filter_on: dataviewQuery(dataviewNode, dataviewName, true),
|
||||
own_filter_off: dataviewQuery(dataviewNode, dataviewName, false),
|
||||
no_filters: dataviewNode.getQuery(Object.keys(dataviewNode.getFilters())
|
||||
.reduce(function(applyFilters, filterId) {
|
||||
applyFilters[filterId] = false;
|
||||
return applyFilters;
|
||||
}, {})
|
||||
)
|
||||
};
|
||||
});
|
||||
if (Object.keys(dataviews).length > 0) {
|
||||
requestMapConfig.dataviews = dataviews;
|
||||
}
|
||||
|
||||
debug('mapconfig output', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
context.analysesResults = analysesResults;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
|
||||
var SKIP_COLUMNS = {
|
||||
'the_geom': true,
|
||||
'the_geom_webmercator': true
|
||||
};
|
||||
|
||||
function skipColumns(columnNames) {
|
||||
return columnNames
|
||||
.filter(function(columnName) { return !SKIP_COLUMNS[columnName]; });
|
||||
}
|
||||
|
||||
var wrappedQueryTpl = 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 wrappedQueryTpl({ _query: node.getQuery(), _columns: _columns.join(', ') });
|
||||
}
|
||||
|
||||
function dataviewQuery(node, dataviewName, ownFilter) {
|
||||
var applyFilters = {};
|
||||
if (!ownFilter) {
|
||||
applyFilters[dataviewName] = false;
|
||||
}
|
||||
|
||||
if (node.type === 'source') {
|
||||
return node.getQuery(applyFilters);
|
||||
}
|
||||
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
|
||||
return wrappedQueryTpl({ _query: node.getQuery(applyFilters), _columns: _columns.join(', ') });
|
||||
}
|
||||
|
||||
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
var analyses = requestMapConfig.analyses || [];
|
||||
|
||||
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);
|
||||
var definition = analysisDefinition;
|
||||
Object.keys(dataviewsFiltersBySourceId).forEach(function(sourceId) {
|
||||
definition = analysisGraph.getDefinitionWith(sourceId, {filters: dataviewsFiltersBySourceId[sourceId] });
|
||||
});
|
||||
|
||||
return definition;
|
||||
});
|
||||
|
||||
return requestMapConfig;
|
||||
}
|
||||
|
||||
function shouldAdaptLayers(requestMapConfig) {
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(getLayerSourceId) ||
|
||||
(Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0) ||
|
||||
requestMapConfig.dataviews;
|
||||
}
|
||||
|
||||
var DATAVIEW_TYPE_2_FILTER_TYPE = {
|
||||
aggregation: 'category',
|
||||
histogram: 'range'
|
||||
};
|
||||
function getFilter(dataview, params) {
|
||||
var type = dataview.type;
|
||||
|
||||
return {
|
||||
type: DATAVIEW_TYPE_2_FILTER_TYPE[type],
|
||||
column: dataview.options.column,
|
||||
params: params
|
||||
};
|
||||
}
|
||||
|
||||
function getLayerSourceId(layer) {
|
||||
return layer.options.source && layer.options.source.id;
|
||||
}
|
||||
|
||||
function getDataviewSourceId(dataview) {
|
||||
return dataview.source && dataview.source.id;
|
||||
}
|
||||
|
||||
function getLayerDataviews(layer, dataviews) {
|
||||
var layerDataviews = [];
|
||||
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
if (layerSourceId) {
|
||||
var dataviewsList = getDataviewsList(dataviews);
|
||||
dataviewsList.forEach(function(dataview) {
|
||||
if (getDataviewSourceId(dataview) === layerSourceId) {
|
||||
layerDataviews.push(dataview);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return layerDataviews;
|
||||
}
|
||||
|
||||
function getDataviewsColumns(dataviews) {
|
||||
return Object.keys(dataviews.reduce(function(columnsDict, dataview) {
|
||||
getDataviewColumns(dataview).forEach(function(columnName) {
|
||||
if (!!columnName) {
|
||||
columnsDict[columnName] = true;
|
||||
}
|
||||
});
|
||||
return columnsDict;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
function getDataviewColumns(dataview) {
|
||||
var columns = [];
|
||||
var options = dataview.options;
|
||||
['column', 'aggregationColumn'].forEach(function(opt) {
|
||||
if (options.hasOwnProperty(opt) && !!options[opt]) {
|
||||
columns.push(options[opt]);
|
||||
}
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
function getDataviewsList(dataviews) {
|
||||
return Object.keys(dataviews).map(function(dataviewKey) { return dataviews[dataviewKey]; });
|
||||
}
|
||||
|
||||
function getDataviewsErrors(dataviews) {
|
||||
var dataviewType = typeof dataviews;
|
||||
if (dataviewType !== 'object') {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "' + dataviewType + '" type found')];
|
||||
}
|
||||
|
||||
if (Array.isArray(dataviews)) {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "array" type found')];
|
||||
}
|
||||
|
||||
var errors = [];
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
if (!dataview.hasOwnProperty('source') || !dataview.source.id) {
|
||||
errors.push(new Error('Dataview "' + dataviewName + '" is missing `source.id` attribute'));
|
||||
}
|
||||
|
||||
if (!dataview.type) {
|
||||
errors.push(new Error('Dataview "' + dataviewName + '" is missing `type` attribute'));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getMissingDataviewsSourceIds(dataviews, sourceId2Node) {
|
||||
var missingDataviewsSourceIds = [];
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
var dataviewSourceId = getDataviewSourceId(dataview);
|
||||
if (!sourceId2Node.hasOwnProperty(dataviewSourceId)) {
|
||||
missingDataviewsSourceIds.push(new AnalysisError('Node with `source.id="' + dataviewSourceId +'"`' +
|
||||
' not found in analyses for dataview "' + dataviewName + '"'));
|
||||
}
|
||||
});
|
||||
|
||||
return missingDataviewsSourceIds;
|
||||
}
|
||||
|
||||
function AnalysisError(message) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
this.type = 'analysis';
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
require('util').inherits(AnalysisError, Error);
|
||||
@@ -0,0 +1,98 @@
|
||||
function DataviewsWidgetsMapConfigAdapter() {
|
||||
}
|
||||
|
||||
module.exports = DataviewsWidgetsMapConfigAdapter;
|
||||
|
||||
|
||||
DataviewsWidgetsMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
if (!shouldAdapt(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
// prepare placeholders for new dataviews created from widgets
|
||||
requestMapConfig.analyses = requestMapConfig.analyses || [];
|
||||
requestMapConfig.dataviews = requestMapConfig.dataviews || {};
|
||||
|
||||
requestMapConfig.layers.forEach(function(layer, index) {
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
|
||||
if (!layer.options.widgets) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layerSourceId && !layer.options.sql) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataviewSourceId = layerSourceId || 'cdb-layer-source-' + index;
|
||||
// Append a new analysis if layer has no source id but sql.
|
||||
if (!layerSourceId) {
|
||||
requestMapConfig.analyses.push(
|
||||
{
|
||||
id: dataviewSourceId,
|
||||
type: 'source',
|
||||
params: {
|
||||
query: layer.options.sql
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
var source = { id: dataviewSourceId };
|
||||
var layerWidgets = layer.options.widgets || {};
|
||||
Object.keys(layerWidgets).forEach(function(widgetId) {
|
||||
var dataview = layerWidgets[widgetId];
|
||||
requestMapConfig.dataviews[widgetId] = {
|
||||
source: source,
|
||||
type: dataview.type,
|
||||
options: dataview.options
|
||||
};
|
||||
});
|
||||
|
||||
layer.options.source = source;
|
||||
|
||||
delete layer.options.sql;
|
||||
// don't delete widgets for now as it might be useful for old clients
|
||||
//delete layer.options.widgets;
|
||||
});
|
||||
|
||||
// filters have to be rewritten also
|
||||
var filters = getFilters(params);
|
||||
var layersFilters = filters.layers || [];
|
||||
filters.dataviews = filters.dataviews || {};
|
||||
|
||||
layersFilters.forEach(function(layerFilters) {
|
||||
Object.keys(layerFilters).forEach(function(filterName) {
|
||||
if (!filters.dataviews.hasOwnProperty(filterName)) {
|
||||
filters.dataviews[filterName] = layerFilters[filterName];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
delete filters.layers;
|
||||
|
||||
params.filters = JSON.stringify(filters);
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
};
|
||||
|
||||
function shouldAdapt(requestMapConfig) {
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(function hasWidgets(layer) {
|
||||
return layer.options && layer.options.widgets && Object.keys(layer.options.widgets).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getLayerSourceId(layer) {
|
||||
return layer.options.source && layer.options.source.id;
|
||||
}
|
||||
|
||||
function getFilters(params) {
|
||||
var filters = {};
|
||||
if (params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
26
lib/cartodb/models/mapconfig/adapter/index.js
Normal file
26
lib/cartodb/models/mapconfig/adapter/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
function MapConfigAdapter(adapters) {
|
||||
this.adapters = Array.isArray(adapters) ? adapters : Array.apply(null, arguments);
|
||||
}
|
||||
|
||||
module.exports = MapConfigAdapter;
|
||||
|
||||
MapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
var i = 0;
|
||||
var tasksLeft = this.adapters.length;
|
||||
|
||||
function next(err, _requestMapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (tasksLeft-- === 0) {
|
||||
return callback(null, _requestMapConfig);
|
||||
}
|
||||
var nextAdapter = self.adapters[i++];
|
||||
nextAdapter.getMapConfig(user, _requestMapConfig, params, context, next);
|
||||
}
|
||||
|
||||
next(null, requestMapConfig);
|
||||
};
|
||||
@@ -1,16 +1,23 @@
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
var Datasource = require('windshaft').Datasource;
|
||||
var Datasource = require('windshaft').model.Datasource;
|
||||
|
||||
function MapConfigNamedLayersAdapter(templateMaps) {
|
||||
function MapConfigNamedLayersAdapter(templateMaps, pgConnection) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = MapConfigNamedLayersAdapter;
|
||||
|
||||
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
|
||||
MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
|
||||
if (!layers) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var adaptLayersQueue = queue(layers.length);
|
||||
|
||||
function adaptLayer(layer, done) {
|
||||
@@ -24,9 +31,9 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
var templateConfigParams = layer.options.config || {};
|
||||
var templateAuthTokens = layer.options.auth_tokens;
|
||||
|
||||
self.templateMaps.getTemplate(username, templateName, function(err, template) {
|
||||
self.templateMaps.getTemplate(user, templateName, function(err, template) {
|
||||
if (err || !template) {
|
||||
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
|
||||
return done(new Error("Template '" + templateName + "' of user '" + user + "' not found"));
|
||||
}
|
||||
|
||||
if (self.templateMaps.isAuthorized(template, templateAuthTokens)) {
|
||||
@@ -92,7 +99,10 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
|
||||
});
|
||||
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
requestMapConfig.layers = layers;
|
||||
context.datasource = datasourceBuilder.build();
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +110,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
dbMetadata.setDBAuth(username, dbAuth, function(err) {
|
||||
this.pgConnection.setDBAuth(user, dbAuth, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -110,7 +120,8 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
|
||||
});
|
||||
} else {
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
context.datasource = datasourceBuilder.build();
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
var step = require('step');
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
|
||||
function MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi) {
|
||||
this.overviewsMetadataApi = overviewsMetadataApi;
|
||||
this.filterStatsApi = filterStatsApi;
|
||||
}
|
||||
|
||||
module.exports = MapConfigOverviewsAdapter;
|
||||
|
||||
MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
var analysesResults = context.analysesResults;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var augmentLayersQueue = queue(layers.length);
|
||||
|
||||
function augmentLayer(layer, done) {
|
||||
if ( layer.type !== 'mapnik' && layer.type !== 'cartodb' ) {
|
||||
return done(null, layer);
|
||||
}
|
||||
self.overviewsMetadataApi.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
|
||||
if (err) {
|
||||
done(err, layer);
|
||||
} else {
|
||||
var query_rewrite_data = { overviews: metadata };
|
||||
step(
|
||||
function collectFiltersData() {
|
||||
var filters, unfiltered_query;
|
||||
if ( layer.options.source && analysesResults && !layer.options.sql_wrap) {
|
||||
var sourceId = layer.options.source.id;
|
||||
var node = _.find(analysesResults, function(a){ return a.rootNode.params.id === sourceId; });
|
||||
if ( node ) {
|
||||
node = node.rootNode;
|
||||
filters = node.getFilters();
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
unfiltered_query = node.getQuery(filters_disabler);
|
||||
query_rewrite_data.filters = filters;
|
||||
query_rewrite_data.unfiltered_query = unfiltered_query;
|
||||
}
|
||||
}
|
||||
this(null, filters, unfiltered_query);
|
||||
},
|
||||
function collectStatsData(err, filters, unfiltered_query) {
|
||||
var next_step = this;
|
||||
if ( filters ) {
|
||||
self.filterStatsApi.getFilterStats(
|
||||
user,
|
||||
unfiltered_query, filters,
|
||||
function(err, stats) {
|
||||
if ( !err ) {
|
||||
query_rewrite_data.filter_stats = stats;
|
||||
}
|
||||
return next_step(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return next_step(null);
|
||||
}
|
||||
},
|
||||
function addDataToLayer(err) {
|
||||
if ( !err && !_.isEmpty(metadata) ) {
|
||||
layer = _.extend({}, layer);
|
||||
layer.options = _.extend({}, layer.options, { query_rewrite_data: query_rewrite_data });
|
||||
}
|
||||
done(null, layer);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function layersAugmentQueueFinish(err, layers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(new Error('Missing layers array from layergroup config'));
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
augmentLayersQueue.defer(augmentLayer, layer);
|
||||
});
|
||||
augmentLayersQueue.awaitAll(layersAugmentQueueFinish);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
function SqlWrapMapConfigAdapter() {
|
||||
}
|
||||
|
||||
module.exports = SqlWrapMapConfigAdapter;
|
||||
|
||||
|
||||
SqlWrapMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
if (requestMapConfig && Array.isArray(requestMapConfig.layers)) {
|
||||
requestMapConfig.layers = requestMapConfig.layers.map(function(layer) {
|
||||
if (layer.options) {
|
||||
var sqlQueryWrap = layer.options.sql_wrap;
|
||||
if (sqlQueryWrap) {
|
||||
var layerSql = layer.options.sql;
|
||||
if (layerSql) {
|
||||
layer.options.sql_raw = layerSql;
|
||||
layer.options.sql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, layerSql);
|
||||
}
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
};
|
||||
180
lib/cartodb/models/mapconfig/adapter/turbo-carto-adapter.js
Normal file
180
lib/cartodb/models/mapconfig/adapter/turbo-carto-adapter.js
Normal file
@@ -0,0 +1,180 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var queue = require('queue-async');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var turboCarto = require('turbo-carto');
|
||||
|
||||
var SubstitutionTokens = require('../../../utils/substitution-tokens');
|
||||
var PostgresDatasource = require('../../../backends/turbo-carto-postgres-datasource');
|
||||
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
function TurboCartoAdapter() {
|
||||
}
|
||||
|
||||
module.exports = TurboCartoAdapter;
|
||||
|
||||
TurboCartoAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var parseCartoQueue = queue(layers.length);
|
||||
|
||||
layers.forEach(function(layer, index) {
|
||||
var layerId = MapConfig.getLayerId(requestMapConfig, index);
|
||||
parseCartoQueue.defer(self._parseCartoCss.bind(self), user, params, layer, index, layerId);
|
||||
});
|
||||
|
||||
parseCartoQueue.awaitAll(function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var errors = results.reduce(function(errors, result) {
|
||||
if (result.error) {
|
||||
errors.push(result.error);
|
||||
}
|
||||
return errors;
|
||||
}, []);
|
||||
if (errors.length > 0) {
|
||||
return callback(errors);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = results.map(function(result) { return result.layer; });
|
||||
context.turboCarto = {
|
||||
layers: results.map(function(result) {
|
||||
return result.meta;
|
||||
})
|
||||
};
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
|
||||
var tokensQueryTpl = dot.template([
|
||||
'WITH input_query AS (',
|
||||
' {{=it._sql}}',
|
||||
'),',
|
||||
'bbox_query AS (',
|
||||
' SELECT ST_SetSRID(ST_Extent(the_geom_webmercator), 3857) as bbox from input_query',
|
||||
'),',
|
||||
'zoom_query as (',
|
||||
' SELECT GREATEST(',
|
||||
' ceil(log(40075017000 / 256 / GREATEST(ST_XMax(bbox) - ST_XMin(bbox), ST_YMax(bbox) - ST_YMin(bbox)))/log(2)),',
|
||||
' 0) as zoom',
|
||||
' FROM bbox_query',
|
||||
'),',
|
||||
'pixel_size_query as (',
|
||||
' SELECT 40075017 * cos(radians(ST_Y(ST_Transform(ST_Centroid(bbox), 4326)))) / 2 ^ ((zoom) + 8) as pixel_size',
|
||||
' FROM bbox_query, zoom_query',
|
||||
'),',
|
||||
'scale_denominator_query as (',
|
||||
' SELECT (pixel_size / 0.00028)::numeric as scale_denominator',
|
||||
' FROM pixel_size_query',
|
||||
')',
|
||||
'select ST_AsText(bbox) bbox, pixel_size, scale_denominator, zoom',
|
||||
'from bbox_query, pixel_size_query, scale_denominator_query, zoom_query'
|
||||
].join('\n'));
|
||||
|
||||
TurboCartoAdapter.prototype._parseCartoCss = function (username, params, layer, layerIndex, layerId, callback) {
|
||||
if (!shouldParseLayerCartocss(layer)) {
|
||||
return callback(null, { layer: layer });
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
function processCallback(err, cartocss, meta) {
|
||||
// Only return turbo-carto errors
|
||||
if (err && err.name === 'TurboCartoError') {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = 400;
|
||||
error.type = 'layer';
|
||||
error.subtype = 'turbo-carto';
|
||||
error.layer = {
|
||||
id: layerId,
|
||||
index: layerIndex,
|
||||
type: layer.type,
|
||||
context: err.context
|
||||
};
|
||||
|
||||
return callback(null, { error: error });
|
||||
}
|
||||
|
||||
// Try to continue in the rest of the cases
|
||||
if (cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
return callback(null, { layer: layer, meta: meta });
|
||||
}
|
||||
|
||||
var layerSql = layer.options.sql;
|
||||
var layerRawSql = layer.options.sql_raw;
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
|
||||
var self = this;
|
||||
var tokensQuery = tokensQueryTpl({_sql: layerRawSql});
|
||||
return pg.query(tokensQuery, function(err, resultSet) {
|
||||
if (err) {
|
||||
return processCallback(err);
|
||||
}
|
||||
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
var result = rows[0] || {};
|
||||
|
||||
var tokens = {
|
||||
bbox: 'ST_SetSRID(ST_GeomFromText(\'' + result.bbox + '\'), 3857)',
|
||||
scale_denominator: result.scale_denominator,
|
||||
pixel_width: result.pixel_size,
|
||||
pixel_height: result.pixel_size
|
||||
};
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
self.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
}, true); // use read-only transaction
|
||||
}
|
||||
|
||||
var tokens = {
|
||||
bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
|
||||
scale_denominator: '500000001',
|
||||
pixel_width: '156412',
|
||||
pixel_height: '156412'
|
||||
};
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
this.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
};
|
||||
|
||||
TurboCartoAdapter.prototype.process = function (psql, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(psql, sql);
|
||||
turboCarto(cartocss, datasource, callback);
|
||||
};
|
||||
|
||||
function shouldParseLayerCartocss(layer) {
|
||||
return layer && layer.options && layer.options.cartocss && layer.options.sql;
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var MapStoreMapConfigProvider = require('./map-store-provider');
|
||||
|
||||
/**
|
||||
* @param {MapConfig} mapConfig
|
||||
* @param {String} user
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {Object} params
|
||||
* @constructor
|
||||
* @type {CreateLayergroupMapConfigProvider}
|
||||
*/
|
||||
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
|
||||
this.mapConfig = mapConfig;
|
||||
this.user = user;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.params = params;
|
||||
this.cacheBuster = params.cache_buster || 0;
|
||||
}
|
||||
|
||||
module.exports = CreateLayergroupMapConfigProvider;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
context.limits = renderLimits;
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
return callback(err, self.mapConfig, self.params, context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getKey = MapStoreMapConfigProvider.prototype.getKey;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigProvider.prototype.getCacheBuster;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;
|
||||
77
lib/cartodb/models/mapconfig/provider/map-store-provider.js
Normal file
77
lib/cartodb/models/mapconfig/provider/map-store-provider.js
Normal file
@@ -0,0 +1,77 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var dot = require('dot');
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
* @param {MapStore} mapStore
|
||||
* @param {String} user
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {Object} params
|
||||
* @constructor
|
||||
* @type {MapStoreMapConfigProvider}
|
||||
*/
|
||||
function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params) {
|
||||
this.mapStore = mapStore;
|
||||
this.user = user;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.params = params;
|
||||
this.token = params.token;
|
||||
this.cacheBuster = params.cache_buster || 0;
|
||||
}
|
||||
|
||||
module.exports = MapStoreMapConfigProvider;
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
context.limits = renderLimits;
|
||||
return null;
|
||||
},
|
||||
function loadMapConfig(err) {
|
||||
assert.ifError(err);
|
||||
self.mapStore.load(self.token, this);
|
||||
},
|
||||
function finish(err, mapConfig) {
|
||||
return callback(err, mapConfig, self.params, context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getKey = function() {
|
||||
return this.createKey(false);
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.getCacheBuster = function() {
|
||||
return this.cacheBuster;
|
||||
};
|
||||
|
||||
MapStoreMapConfigProvider.prototype.filter = function(key) {
|
||||
var regex = new RegExp('^' + this.createKey(true) + '.*');
|
||||
return key && key.match(regex);
|
||||
};
|
||||
|
||||
// Configure bases for cache keys suitable for string interpolation
|
||||
var baseKey = '{{=it.dbname}}:{{=it.token}}';
|
||||
var rendererKey = baseKey + ':{{=it.dbuser}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
|
||||
var baseKeyTpl = dot.template(baseKey);
|
||||
var rendererKeyTpl = dot.template(rendererKey);
|
||||
|
||||
MapStoreMapConfigProvider.prototype.createKey = function(base) {
|
||||
var tplValues = _.defaults({}, this.params, {
|
||||
dbname: '',
|
||||
token: '',
|
||||
dbuser: '',
|
||||
format: '',
|
||||
layer: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
|
||||
};
|
||||
288
lib/cartodb/models/mapconfig/provider/named-map-provider.js
Normal file
288
lib/cartodb/models/mapconfig/provider/named-map-provider.js
Normal file
@@ -0,0 +1,288 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var dot = require('dot');
|
||||
var step = require('step');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
var templateName = require('../../../backends/template_maps').templateName;
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @type {NamedMapMapConfigProvider}
|
||||
*/
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter,
|
||||
owner, templateId, config, authToken, params) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
|
||||
this.owner = owner;
|
||||
this.templateName = templateName(templateId);
|
||||
this.config = config;
|
||||
this.authToken = authToken;
|
||||
this.params = params;
|
||||
|
||||
this.cacheBuster = Date.now();
|
||||
|
||||
// use template after call to mapConfig
|
||||
this.template = null;
|
||||
|
||||
this.affectedTablesAndLastUpdate = null;
|
||||
|
||||
// providing
|
||||
this.err = null;
|
||||
this.mapConfig = null;
|
||||
this.rendererParams = null;
|
||||
this.context = {};
|
||||
this.analysesResults = [];
|
||||
}
|
||||
|
||||
module.exports = NamedMapMapConfigProvider;
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
if (!!this.err || this.mapConfig !== null) {
|
||||
return callback(this.err, this.mapConfig, this.rendererParams, this.context);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var mapConfig = null;
|
||||
var rendererParams;
|
||||
var apiKey;
|
||||
|
||||
var context = {};
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
self.getTemplate(this);
|
||||
},
|
||||
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);
|
||||
|
||||
apiKey = _apiKey;
|
||||
|
||||
var templateParams = {};
|
||||
if (self.config) {
|
||||
try {
|
||||
templateParams = _.isString(self.config) ? JSON.parse(self.config) : self.config;
|
||||
} catch (e) {
|
||||
throw new Error('malformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return templateParams;
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: self.owner,
|
||||
db: {
|
||||
host: rendererParams.dbhost,
|
||||
port: rendererParams.dbport,
|
||||
dbname: rendererParams.dbname,
|
||||
user: rendererParams.dbuser,
|
||||
pass: rendererParams.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: self.owner,
|
||||
apiKey: apiKey
|
||||
}
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(self.owner, requestMapConfig, rendererParams, context, this);
|
||||
},
|
||||
function prepareContextLimits(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, context.datasource);
|
||||
self.analysesResults = context.analysesResults || [];
|
||||
self.rendererParams = rendererParams;
|
||||
self.context = context;
|
||||
self.context.limits = renderLimits || {};
|
||||
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getTemplate = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (!!this.err || this.template !== null) {
|
||||
return callback(this.err, this.template);
|
||||
}
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
self.templateMaps.getTemplate(self.owner, self.templateName, this);
|
||||
},
|
||||
function checkExists(err, tpl) {
|
||||
assert.ifError(err);
|
||||
if (!tpl) {
|
||||
var notFoundErr = new Error(
|
||||
"Template '" + self.templateName + "' of user '" + self.owner + "' not found"
|
||||
);
|
||||
notFoundErr.http_status = 404;
|
||||
throw notFoundErr;
|
||||
}
|
||||
return tpl;
|
||||
},
|
||||
function checkAuthorized(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(tpl, self.authToken);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
var authorizationFailedErr = new Error('Failed to authorize template');
|
||||
authorizationFailedErr.http_status = 403;
|
||||
throw authorizationFailedErr;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
var unauthorizedErr = new Error('Unauthorized template instantiation');
|
||||
unauthorizedErr.http_status = 403;
|
||||
throw unauthorizedErr;
|
||||
}
|
||||
|
||||
return tpl;
|
||||
},
|
||||
function cacheAndReturnTemplate(err, template) {
|
||||
self.err = err;
|
||||
self.template = template;
|
||||
return callback(self.err, self.template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getKey = function() {
|
||||
return this.createKey(false);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getCacheBuster = function() {
|
||||
return this.cacheBuster;
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.reset = function() {
|
||||
this.template = null;
|
||||
|
||||
this.affectedTablesAndLastUpdate = null;
|
||||
|
||||
this.err = null;
|
||||
this.mapConfig = null;
|
||||
|
||||
this.cacheBuster = Date.now();
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.filter = function(key) {
|
||||
var regex = new RegExp('^' + this.createKey(true) + '.*');
|
||||
return key && key.match(regex);
|
||||
};
|
||||
|
||||
// Configure bases for cache keys suitable for string interpolation
|
||||
var baseKey = '{{=it.dbname}}:{{=it.owner}}:{{=it.templateName}}';
|
||||
var rendererKey = baseKey + ':{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
|
||||
|
||||
var baseKeyTpl = dot.template(baseKey);
|
||||
var rendererKeyTpl = dot.template(rendererKey);
|
||||
|
||||
NamedMapMapConfigProvider.prototype.createKey = function(base) {
|
||||
var tplValues = _.defaults({}, this.params, {
|
||||
dbname: '',
|
||||
owner: this.owner,
|
||||
templateName: this.templateName,
|
||||
authToken: this.authToken || '',
|
||||
configHash: configHash(this.config),
|
||||
layer: '',
|
||||
scale_factor: 1
|
||||
});
|
||||
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
|
||||
};
|
||||
|
||||
function configHash(config) {
|
||||
if (!config) {
|
||||
return '';
|
||||
}
|
||||
return crypto.createHash('md5').update(JSON.stringify(config)).digest('hex').substring(0,8);
|
||||
}
|
||||
|
||||
module.exports.configHash = configHash;
|
||||
|
||||
NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getTemplateName = function() {
|
||||
return this.templateName;
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (this.affectedTablesAndLastUpdate !== null) {
|
||||
return callback(null, this.affectedTablesAndLastUpdate);
|
||||
}
|
||||
|
||||
step(
|
||||
function getMapConfig() {
|
||||
self.getMapConfig(this);
|
||||
},
|
||||
function getSql(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
return mapConfig.getLayers().map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
},
|
||||
function getAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
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;
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,45 +1,19 @@
|
||||
var fs = require('fs');
|
||||
var step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
function HealthCheck(disableFile) {
|
||||
this.disableFile = disableFile;
|
||||
}
|
||||
|
||||
module.exports = HealthCheck;
|
||||
|
||||
|
||||
HealthCheck.prototype.check = function(config, callback) {
|
||||
|
||||
var result = {
|
||||
redis: {
|
||||
ok: false
|
||||
},
|
||||
mapnik: {
|
||||
ok: false
|
||||
},
|
||||
tile: {
|
||||
ok: false
|
||||
}
|
||||
};
|
||||
|
||||
step(
|
||||
function getManualDisable() {
|
||||
fs.readFile(global.environment.disabled_file, this);
|
||||
},
|
||||
function handleDisabledFile(err, data) {
|
||||
var next = this;
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
if (!!data) {
|
||||
err = new Error(data);
|
||||
err.http_status = 503;
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
function handleResult(err) {
|
||||
callback(err, result);
|
||||
HealthCheck.prototype.check = function(callback) {
|
||||
fs.readFile(this.disableFile, function handleDisabledFile(err, data) {
|
||||
var disabledError = null;
|
||||
if (!err) {
|
||||
disabledError = new Error(data || 'Unknown error');
|
||||
disabledError.http_status = 503;
|
||||
}
|
||||
);
|
||||
return callback(disabledError);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<Map
|
||||
background-color="#c33"
|
||||
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
|
||||
</Map>
|
||||
377
lib/cartodb/server.js
Normal file
377
lib/cartodb/server.js
Normal file
@@ -0,0 +1,377 @@
|
||||
var express = require('express');
|
||||
var bodyParser = require('body-parser');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
|
||||
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
var StatsClient = require('./stats/client');
|
||||
var Profiler = require('./stats/profiler_proxy');
|
||||
var RendererStatsReporter = require('./stats/reporter/renderer');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
var mapnik = windshaft.mapnik;
|
||||
|
||||
var TemplateMaps = require('./backends/template_maps.js');
|
||||
var OverviewsMetadataApi = require('./api/overviews_metadata_api');
|
||||
var FilterStatsApi = require('./api/filter_stats_api');
|
||||
var UserLimitsApi = require('./api/user_limits_api');
|
||||
var AuthApi = require('./api/auth_api');
|
||||
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
|
||||
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 SqlWrapMapConfigAdapter = require('./models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
|
||||
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
|
||||
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
|
||||
name: 'windshaft-server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}));
|
||||
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
var filterStatsApi = new FilterStatsApi(pgQueryRunner);
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0
|
||||
}
|
||||
});
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends(serverOptions));
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
global.logger.warn(logMessage);
|
||||
} else {
|
||||
global.logger.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
serverOptions.grainstore.mapnik_version = mapnikVersion(serverOptions);
|
||||
|
||||
validateOptions(serverOptions);
|
||||
|
||||
bootstrapFonts(serverOptions);
|
||||
|
||||
// initialize express server
|
||||
var app = bootstrap(serverOptions);
|
||||
// Extend windshaft with all the elements of the options object
|
||||
_.extend(app, serverOptions);
|
||||
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: serverOptions.grainstore.default_layergroup_ttl
|
||||
});
|
||||
|
||||
var onTileErrorStrategy;
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var rendererFactory = new windshaft.renderer.Factory({
|
||||
onTileErrorStrategy: onTileErrorStrategy,
|
||||
mapnik: {
|
||||
redisPool: redisPool,
|
||||
grainstore: serverOptions.grainstore,
|
||||
mapnik: serverOptions.renderer.mapnik
|
||||
},
|
||||
http: serverOptions.renderer.http
|
||||
});
|
||||
|
||||
// initialize render cache
|
||||
var rendererCacheOpts = _.defaults(serverOptions.renderCache || {}, {
|
||||
ttl: 60000, // 60 seconds TTL by default
|
||||
statsInterval: 60000 // reports stats every milliseconds defined here
|
||||
});
|
||||
var rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
|
||||
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
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(metadataBackend, serverOptions.analysis);
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsApi,
|
||||
mapConfigAdapter
|
||||
);
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
|
||||
});
|
||||
|
||||
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
|
||||
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
|
||||
|
||||
var versions = getAndValidateVersions(serverOptions);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
new controller.Layergroup(
|
||||
authApi,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache,
|
||||
analysisBackend
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
authApi,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
authApi,
|
||||
pgConnection,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi,
|
||||
metadataBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
|
||||
|
||||
new controller.Analyses(authApi, pgConnection).register(app);
|
||||
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
function validateOptions(opts) {
|
||||
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
|
||||
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
|
||||
}
|
||||
}
|
||||
|
||||
function getAndValidateVersions(options) {
|
||||
// jshint undef:false
|
||||
var warn = console.warn.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var packageDefinition = require('../../package.json');
|
||||
|
||||
var declaredDependencies = packageDefinition.dependencies || {};
|
||||
var installedDependenciesVersions = {
|
||||
camshaft: require('camshaft').version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
'turbo-carto': require('turbo-carto').version,
|
||||
windshaft: windshaft.version,
|
||||
windshaft_cartodb: packageDefinition.version
|
||||
};
|
||||
|
||||
var dependenciesToValidate = ['camshaft', 'turbo-carto', 'windshaft'];
|
||||
dependenciesToValidate.forEach(function(depName) {
|
||||
var declaredDependencyVersion = declaredDependencies[depName];
|
||||
var installedDependencyVersion = installedDependenciesVersions[depName];
|
||||
if (declaredDependencyVersion !== installedDependencyVersion) {
|
||||
warn(
|
||||
'Dependency="%s" installed version="%s" does not match declared version="%s". Check your installation.',
|
||||
depName, installedDependencyVersion, declaredDependencyVersion
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Be nice and warn if configured mapnik version is != installed mapnik version
|
||||
if (mapnik.versions.mapnik !== options.grainstore.mapnik_version) {
|
||||
warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + options.grainstore.mapnik_version + ')');
|
||||
}
|
||||
|
||||
return installedDependenciesVersions;
|
||||
}
|
||||
|
||||
function bootstrapFonts(opts) {
|
||||
// Set carto renderer configuration for MMLStore
|
||||
opts.grainstore.carto_env = opts.grainstore.carto_env || {};
|
||||
var cenv = opts.grainstore.carto_env;
|
||||
cenv.validation_data = cenv.validation_data || {};
|
||||
if ( ! cenv.validation_data.fonts ) {
|
||||
mapnik.register_system_fonts();
|
||||
mapnik.register_default_fonts();
|
||||
cenv.validation_data.fonts = _.keys(mapnik.fontFiles());
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrap(opts) {
|
||||
var app;
|
||||
if (_.isObject(opts.https)) {
|
||||
// use https if possible
|
||||
app = express.createServer(opts.https);
|
||||
} else {
|
||||
// fall back to http by default
|
||||
app = express();
|
||||
}
|
||||
app.enable('jsonp callback');
|
||||
app.disable('x-powered-by');
|
||||
app.disable('etag');
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
|
||||
req.context = req.context || {};
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: global.statsClient,
|
||||
profile: opts.useProfiler
|
||||
});
|
||||
|
||||
if (global.environment && global.environment.api_hostname) {
|
||||
res.set('X-Served-By-Host', global.environment.api_hostname);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
app.use(function(err, req, res, next) {
|
||||
if (err) {
|
||||
if (err.name === 'SyntaxError') {
|
||||
res.status(400).json({ errors: [err.name + ': ' + err.message] });
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
setupLogger(app, opts);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function setupLogger(app, opts) {
|
||||
if (global.log4js && opts.log_format) {
|
||||
var loggerOpts = {
|
||||
// Allowing for unbuffered logging is mainly
|
||||
// used to avoid hanging during unit testing.
|
||||
// TODO: provide an explicit teardown function instead,
|
||||
// releasing any event handler or timer set by
|
||||
// this component.
|
||||
buffer: !opts.unbuffered_logging,
|
||||
// optional log format
|
||||
format: opts.log_format
|
||||
};
|
||||
app.use(global.log4js.connectLogger(global.log4js.getLogger(), _.defaults(loggerOpts, {level: 'info'})));
|
||||
}
|
||||
}
|
||||
|
||||
function surrogateKeysCacheBackends(serverOptions) {
|
||||
var cacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
cacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (serverOptions.fastly &&
|
||||
!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
cacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
return cacheBackends;
|
||||
}
|
||||
|
||||
function mapnikVersion(opts) {
|
||||
return opts.grainstore.mapnik_version || mapnik.versions.mapnik;
|
||||
}
|
||||
@@ -1,646 +1,121 @@
|
||||
var os = require('os');
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var OverviewsQueryRewriter = require('./utils/overviews_query_rewriter');
|
||||
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var PgQueryRunner = require('./backends/pg_query_runner');
|
||||
var PgConnection = require('./backends/pg_connection');
|
||||
var TemplateMaps = require('./template_maps.js');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter');
|
||||
var CdbRequest = require('./models/cdb_request');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
statsInterval: 60000,
|
||||
mapnik: {
|
||||
poolSize: 8,
|
||||
metatile: 2,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
});
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
];
|
||||
rendererConfig.mapnik.queryRewriter = overviewsQueryRewriter;
|
||||
|
||||
module.exports = function(redisPool) {
|
||||
redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'}));
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
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 cartoData = require('cartodb-redis')({ pool: redisPool });
|
||||
var lzmaWorker = new LZMA();
|
||||
var pgConnection = new PgConnection(cartoData);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var cdbRequest = new CdbRequest();
|
||||
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'
|
||||
},
|
||||
logger: {
|
||||
filename: undefined
|
||||
},
|
||||
limits: {}
|
||||
});
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
statsInterval: 60000,
|
||||
mapnik: {
|
||||
poolSize: 8,
|
||||
metatile: 2,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
});
|
||||
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',
|
||||
|
||||
var me = {
|
||||
// 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)',
|
||||
|
||||
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
|
||||
datasource: global.environment.postgres,
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
use_workers: rendererConfig.mapnik.useCartocssWorkers || false,
|
||||
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
|
||||
},
|
||||
renderer: {
|
||||
mapnik: rendererConfig.mapnik,
|
||||
http: rendererConfig.http
|
||||
logger: {
|
||||
filename: analysisConfig.logger.filename
|
||||
},
|
||||
redis: global.environment.redis,
|
||||
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
|
||||
};
|
||||
|
||||
// Do not send unwatch on release
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
me.redis.unwatchOnRelease = false;
|
||||
|
||||
// Re-use redisPool
|
||||
me.redis.pool = redisPool;
|
||||
|
||||
// Re-use pgConnection
|
||||
me.pgConnection = pgConnection;
|
||||
// Re-use pgQueryRunner
|
||||
me.pgQueryRunner = pgQueryRunner;
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
me.templateMaps = templateMaps;
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
/* This whole block is about generating X-Cache-Channel { */
|
||||
|
||||
// TODO: review lifetime of elements of this cache
|
||||
// NOTE: by-token indices should only be dropped when
|
||||
// the corresponding layegroup is dropped, because
|
||||
// we have no SQL after layer creation.
|
||||
me.channelCache = {};
|
||||
|
||||
me.buildCacheChannel = function (dbName, tableNames){
|
||||
return dbName + ':' + tableNames.join(',');
|
||||
};
|
||||
|
||||
me.generateCacheChannel = function(app, req, callback){
|
||||
// Build channelCache key
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = [ dbName, req.params.token ].join(':');
|
||||
|
||||
// no token means no tables associated
|
||||
if (!req.params.token) {
|
||||
return callback(null, this.buildCacheChannel(dbName, []));
|
||||
}
|
||||
|
||||
step(
|
||||
function checkCached() {
|
||||
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
||||
return callback(null, me.channelCache[cacheKey]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function extractSQL(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
// TODO: cached cache channel for token-based access should
|
||||
// be constructed at renderer cache creation time
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
|
||||
if ( ! app.mapStore ) {
|
||||
throw new Error('missing channel cache for token ' + req.params.token);
|
||||
}
|
||||
var mapStore = app.mapStore;
|
||||
step(
|
||||
function loadFromStore() {
|
||||
mapStore.load(req.params.token, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('mapStore_load');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
queryTablesApi.getAffectedTablesInQuery(cdbRequest.userByReq(req), sql, this); // in addCacheChannel
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
assert.ifError(err);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('affectedTables');
|
||||
}
|
||||
|
||||
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
return cacheChannel;
|
||||
},
|
||||
function finish(err, cacheChannel) {
|
||||
callback(err, cacheChannel);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Set the cache chanel info to invalidate the cache on the frontend server
|
||||
//
|
||||
// @param req The request object.
|
||||
// The function will have no effect unless req.res exists.
|
||||
// It is expected that req.params contains 'table' and 'dbname'
|
||||
//
|
||||
// @param cb function(err, channel) will be called when ready.
|
||||
// the channel parameter will be null if nothing was added
|
||||
//
|
||||
me.addCacheChannel = function(app, req, cb) {
|
||||
// skip non-GET requests, or requests for which there's no response
|
||||
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
|
||||
if (req.profiler) {
|
||||
req.profiler.start('addCacheChannel');
|
||||
}
|
||||
var res = req.res;
|
||||
if ( req.params.token ) {
|
||||
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
||||
} else {
|
||||
var ttl = global.environment.varnish.ttl || 86400;
|
||||
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
|
||||
}
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if ( req.params.cache_buster ) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
// FIXME: store lastModified in the cache channel instead
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.header('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
me.generateCacheChannel(app, req, function(err, channel){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('generateCacheChannel');
|
||||
req.profiler.end();
|
||||
}
|
||||
if ( ! err ) {
|
||||
res.header('X-Cache-Channel', channel);
|
||||
cb(null, channel);
|
||||
} else {
|
||||
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
|
||||
// TODO: evaluate if we should bubble up the error instead
|
||||
cb(null, 'ERROR');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
me.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
me.renderCache.beforeRendererCreate = function(req, callback) {
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
var rendererOptions = {};
|
||||
|
||||
step(
|
||||
function getLimits(err) {
|
||||
assert.ifError(err);
|
||||
cartoData.getTilerRenderLimit(user, this);
|
||||
},
|
||||
function handleTilerLimits(err, renderLimit) {
|
||||
assert.ifError(err);
|
||||
rendererOptions.limits = {
|
||||
cacheOnTimeout: rendererConfig.mapnik.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || rendererConfig.mapnik.limits.render || 0
|
||||
};
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, rendererOptions);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
|
||||
mapConfigNamedLayersAdapter.getLayers(cdbRequest.userByReq(req), requestMapConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
return callback(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
|
||||
var token = response.layergroupid;
|
||||
|
||||
var username = cdbRequest.userByReq(req);
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
var serverMetadata = global.environment.serverMetadata;
|
||||
if (serverMetadata) {
|
||||
_.extend(response, serverMetadata);
|
||||
}
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asyncronously
|
||||
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
}
|
||||
if ( err ) {
|
||||
console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = mapconfig.layers.map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = dbName + ':' + token;
|
||||
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
if (req.res && req.method == 'GET') {
|
||||
var res = req.res;
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.header('Last-Modified', (new Date()).toUTCString());
|
||||
res.header('X-Cache-Channel', cacheChannel);
|
||||
}
|
||||
|
||||
// last update for layergroup cache buster
|
||||
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
|
||||
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/* X-Cache-Channel generation } */
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
me.authorizedBySigner = function(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
return callback(null, null); // no signer requested
|
||||
}
|
||||
|
||||
var signer = req.params.signer;
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
var mapStore = req.app.mapStore;
|
||||
if (!mapStore) {
|
||||
throw new Error('Unable to retrieve map configuration token');
|
||||
}
|
||||
|
||||
mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized ? signer : null);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
me.authorizedByAPIKey = function(req, callback)
|
||||
{
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
callback(null, 0); // no api key, no authorization...
|
||||
return;
|
||||
}
|
||||
//console.log("given ApiKey: " + givenKey);
|
||||
var user = cdbRequest.userByReq(req);
|
||||
step(
|
||||
function (){
|
||||
cartoData.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return ( val && givenKey == val ) ? 1 : 0;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
me.authorize = function(req, callback) {
|
||||
var that = this;
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function (){
|
||||
that.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (authorized !== 1) {
|
||||
// not authorized by api_key,
|
||||
// check if authorized by signer
|
||||
that.authorizedBySigner(req, this);
|
||||
return;
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, signed_by){
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! signed_by ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
callback(null, true); // authorized so far
|
||||
return;
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
pgConnection.setDBAuth(signed_by, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.setDBParams = function(cdbuser, params, callback) {
|
||||
step(
|
||||
function setAuth() {
|
||||
pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
me.req2params = function(req, callback){
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
me.req2params(req, callback);
|
||||
} catch (err) {
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
if ( req.params.token ) {
|
||||
//console.log("Request parameters include token " + req.params.token);
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' +
|
||||
user + '"');
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
me.authorize(req, this);
|
||||
},
|
||||
function gatekeep(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) { callback(err, req); return; }
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return me;
|
||||
limits: analysisConfig.limits
|
||||
},
|
||||
// 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
|
||||
};
|
||||
|
||||
73
lib/cartodb/stats/client.js
Normal file
73
lib/cartodb/stats/client.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var _ = require('underscore');
|
||||
var debug = require('debug')('windshaft:stats_client');
|
||||
var StatsD = require('node-statsd').StatsD;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Returns an StatsD instance or an stub object that replicates the StatsD public interface so there is no need to
|
||||
* keep checking if the stats_client is instantiated or not.
|
||||
*
|
||||
* The first call to this method implies all future calls will use the config specified in the very first call.
|
||||
*
|
||||
* TODO: It's far from ideal to use make this a singleton, improvement desired.
|
||||
* We proceed this way to be able to use StatsD from several places sharing one single StatsD instance.
|
||||
*
|
||||
* @param config Configuration for StatsD, if undefined it will return an stub
|
||||
* @returns {StatsD|Object}
|
||||
*/
|
||||
getInstance: function(config) {
|
||||
|
||||
if (!this.instance) {
|
||||
|
||||
var instance;
|
||||
|
||||
if (config) {
|
||||
instance = new StatsD(config);
|
||||
instance.last_error = { msg: '', count: 0 };
|
||||
instance.socket.on('error', function (err) {
|
||||
var last_err = instance.last_error;
|
||||
var last_msg = last_err.msg;
|
||||
var this_msg = '' + err;
|
||||
if (this_msg !== last_msg) {
|
||||
debug("statsd client socket error: " + err);
|
||||
instance.last_error.count = 1;
|
||||
instance.last_error.msg = this_msg;
|
||||
} else {
|
||||
++last_err.count;
|
||||
if (!last_err.interval) {
|
||||
instance.last_error.interval = setInterval(function () {
|
||||
var count = instance.last_error.count;
|
||||
if (count > 1) {
|
||||
debug("last statsd client socket error repeated " + count + " times");
|
||||
instance.last_error.count = 1;
|
||||
clearInterval(instance.last_error.interval);
|
||||
instance.last_error.interval = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var stubFunc = function (stat, value, sampleRate, callback) {
|
||||
if (_.isFunction(callback)) {
|
||||
callback(null, 0);
|
||||
}
|
||||
};
|
||||
instance = {
|
||||
timing: stubFunc,
|
||||
increment: stubFunc,
|
||||
decrement: stubFunc,
|
||||
gauge: stubFunc,
|
||||
unique: stubFunc,
|
||||
set: stubFunc,
|
||||
sendAll: stubFunc,
|
||||
send: stubFunc
|
||||
};
|
||||
}
|
||||
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
};
|
||||
53
lib/cartodb/stats/profiler_proxy.js
Normal file
53
lib/cartodb/stats/profiler_proxy.js
Normal file
@@ -0,0 +1,53 @@
|
||||
var Profiler = require('step-profiler');
|
||||
|
||||
/**
|
||||
* Proxy to encapsulate node-step-profiler module so there is no need to check if there is an instance
|
||||
*/
|
||||
function ProfilerProxy(opts) {
|
||||
this.profile = !!opts.profile;
|
||||
|
||||
this.profiler = null;
|
||||
if (!!opts.profile) {
|
||||
this.profiler = new Profiler({statsd_client: opts.statsd_client});
|
||||
}
|
||||
}
|
||||
|
||||
ProfilerProxy.prototype.done = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.done(what);
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.end = function() {
|
||||
if (this.profile) {
|
||||
this.profiler.end();
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.start = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.start(what);
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.add = function(what) {
|
||||
if (this.profile) {
|
||||
this.profiler.add(what || {});
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.sendStats = function() {
|
||||
if (this.profile) {
|
||||
this.profiler.sendStats();
|
||||
}
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.toString = function() {
|
||||
return this.profile ? this.profiler.toString() : "";
|
||||
};
|
||||
|
||||
ProfilerProxy.prototype.toJSONString = function() {
|
||||
return this.profile ? this.profiler.toJSONString() : "{}";
|
||||
};
|
||||
|
||||
module.exports = ProfilerProxy;
|
||||
82
lib/cartodb/stats/reporter/renderer.js
Normal file
82
lib/cartodb/stats/reporter/renderer.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// - Reports stats about:
|
||||
// * Total number of renderers
|
||||
// * For mapnik renderers:
|
||||
// - the mapnik-pool status: count, unused and waiting
|
||||
// - the internally cached objects: png and grid
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
function RendererStatsReporter(rendererCache, statsInterval) {
|
||||
this.rendererCache = rendererCache;
|
||||
this.statsInterval = statsInterval || 6e4;
|
||||
this.renderersStatsIntervalId = null;
|
||||
}
|
||||
|
||||
module.exports = RendererStatsReporter;
|
||||
|
||||
RendererStatsReporter.prototype.start = function() {
|
||||
var self = this;
|
||||
this.renderersStatsIntervalId = setInterval(function() {
|
||||
var rendererCacheEntries = self.rendererCache.renderers;
|
||||
|
||||
if (!rendererCacheEntries) {
|
||||
return null;
|
||||
}
|
||||
|
||||
global.statsClient.gauge('windshaft.rendercache.count', _.keys(rendererCacheEntries).length);
|
||||
|
||||
var renderersStats = _.reduce(rendererCacheEntries, function(_rendererStats, cacheEntry) {
|
||||
var stats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats();
|
||||
if (!stats) {
|
||||
return _rendererStats;
|
||||
}
|
||||
|
||||
_rendererStats.pool.count += stats.pool.count;
|
||||
_rendererStats.pool.unused += stats.pool.unused;
|
||||
_rendererStats.pool.waiting += stats.pool.waiting;
|
||||
|
||||
_rendererStats.cache.grid += stats.cache.grid;
|
||||
_rendererStats.cache.png += stats.cache.png;
|
||||
|
||||
return _rendererStats;
|
||||
},
|
||||
{
|
||||
pool: {
|
||||
count: 0,
|
||||
unused: 0,
|
||||
waiting: 0
|
||||
},
|
||||
cache: {
|
||||
png: 0,
|
||||
grid: 0
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
global.statsClient.gauge('windshaft.mapnik-cache.png', renderersStats.cache.png);
|
||||
global.statsClient.gauge('windshaft.mapnik-cache.grid', renderersStats.cache.grid);
|
||||
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.count', renderersStats.pool.count);
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.unused', renderersStats.pool.unused);
|
||||
global.statsClient.gauge('windshaft.mapnik-pool.waiting', renderersStats.pool.waiting);
|
||||
}, this.statsInterval);
|
||||
|
||||
this.rendererCache.on('err', rendererCacheErrorListener);
|
||||
this.rendererCache.on('gc', gcTimingListener);
|
||||
};
|
||||
|
||||
function rendererCacheErrorListener() {
|
||||
global.statsClient.increment('windshaft.rendercache.error');
|
||||
}
|
||||
|
||||
function gcTimingListener(gcTime) {
|
||||
global.statsClient.timing('windshaft.rendercache.gc', gcTime);
|
||||
}
|
||||
|
||||
RendererStatsReporter.prototype.stop = function() {
|
||||
this.rendererCache.removeListener('err', rendererCacheErrorListener);
|
||||
this.rendererCache.removeListener('gc', gcTimingListener);
|
||||
|
||||
clearInterval(this.renderersStatsIntervalId);
|
||||
this.renderersStatsIntervalId = null;
|
||||
};
|
||||
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 AnalysisFilter = require('../models/filter/analysis');
|
||||
|
||||
// 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 analysisFilter = new AnalysisFilter(filters);
|
||||
query = analysisFilter.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));
|
||||
}
|
||||
29
lib/cartodb/utils/substitution-tokens.js
Normal file
29
lib/cartodb/utils/substitution-tokens.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var SUBSTITUTION_TOKENS = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
var SubstitutionTokens = {
|
||||
tokens: function(sql) {
|
||||
return Object.keys(SUBSTITUTION_TOKENS).filter(function(tokenName) {
|
||||
return !!sql.match(SUBSTITUTION_TOKENS[tokenName]);
|
||||
});
|
||||
},
|
||||
|
||||
hasTokens: function(sql) {
|
||||
return this.tokens(sql).length > 0;
|
||||
},
|
||||
|
||||
replace: function(sql, replaceValues) {
|
||||
Object.keys(replaceValues).forEach(function(token) {
|
||||
if (SUBSTITUTION_TOKENS[token]) {
|
||||
sql = sql.replace(SUBSTITUTION_TOKENS[token], replaceValues[token]);
|
||||
}
|
||||
});
|
||||
return sql;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SubstitutionTokens;
|
||||
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
|
||||
};
|
||||
5045
npm-shrinkwrap.json
generated
5045
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user