Compare commits
2001 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c792421687 | ||
|
|
15135b475c | ||
|
|
fd9f935676 | ||
|
|
71f5886a4d | ||
|
|
bc8c9f973c | ||
|
|
ec40614f4b | ||
|
|
5ed1a3a2d1 | ||
|
|
0aa5f394e2 | ||
|
|
2e1a3c7fb1 | ||
|
|
27eb00223d | ||
|
|
8d46780006 | ||
|
|
6ffd2c090e | ||
|
|
3995787c02 | ||
|
|
ddb1b0c0d8 | ||
|
|
a03d268260 | ||
|
|
5c491a25cf | ||
|
|
92be27e700 | ||
|
|
6b61f5e168 | ||
|
|
d79f1b41d0 | ||
|
|
e039204638 | ||
|
|
dc1becd15c | ||
|
|
a121fd75ab | ||
|
|
f85417a886 | ||
|
|
8ad72ff2ce | ||
|
|
4dd6bc466a | ||
|
|
c119c92de6 | ||
|
|
a3f7acb213 | ||
|
|
0f14ed55db | ||
|
|
528395103b | ||
|
|
288cd9584f | ||
|
|
cf82e1954e | ||
|
|
3b00cffc3b | ||
|
|
95bf39cada | ||
|
|
f9ad3c8acf | ||
|
|
28f70f6877 | ||
|
|
d5c5d07507 | ||
|
|
b646f71394 | ||
|
|
38fe2169aa | ||
|
|
a749d4fb43 | ||
|
|
b9198b59a1 | ||
|
|
3102d895f2 | ||
|
|
b60a69e7d2 | ||
|
|
3937b8c271 | ||
|
|
b32a073ac3 | ||
|
|
afd4ad500f | ||
|
|
cb17bba3f5 | ||
|
|
5b7341c0e9 | ||
|
|
d65565c091 | ||
|
|
360b98254b | ||
|
|
43a603922d | ||
|
|
74116523b4 | ||
|
|
6cddec562a | ||
|
|
22086ba914 | ||
|
|
a68618c336 | ||
|
|
578f543c01 | ||
|
|
49735308de | ||
|
|
2444b4c008 | ||
|
|
bf250e592a | ||
|
|
f6c8796c8a | ||
|
|
649f8d701e | ||
|
|
568e428a58 | ||
|
|
ff00fed43e | ||
|
|
561bc8aef0 | ||
|
|
e49ecda321 | ||
|
|
18525a60cd | ||
|
|
b8d3971c8a | ||
|
|
23839f5b4a | ||
|
|
f235dcdeda | ||
|
|
9c21194c68 | ||
|
|
7acbfc1e9b | ||
|
|
6f9580bae2 | ||
|
|
3583e064be | ||
|
|
9e14185990 | ||
|
|
a5c83edef6 | ||
|
|
04d0f2e530 | ||
|
|
e206a1bca3 | ||
|
|
b115bca07e | ||
|
|
07b9decb03 | ||
|
|
02c8e28494 | ||
|
|
d28744a5e3 | ||
|
|
a19e9a79b8 | ||
|
|
4d7eb555a8 | ||
|
|
6f9f53dd03 | ||
|
|
63bc8f75b9 | ||
|
|
adeffd2018 | ||
|
|
b2da00900f | ||
|
|
0c6d5a1e18 | ||
|
|
6945cfc93c | ||
|
|
7b53b7c30a | ||
|
|
d073f7e3dd | ||
|
|
210f5b01ec | ||
|
|
1dda183a31 | ||
|
|
0eadfe6ee9 | ||
|
|
c37e3f173d | ||
|
|
107a97aa9e | ||
|
|
219d2c9044 | ||
|
|
1e89821d97 | ||
|
|
29c6505252 | ||
|
|
7d8d05b865 | ||
|
|
afeb91dc86 | ||
|
|
b7b3392bdd | ||
|
|
b60116410a | ||
|
|
ffe19827fd | ||
|
|
48c28aea0b | ||
|
|
62d66f2dbc | ||
|
|
e644201756 | ||
|
|
481a5928c4 | ||
|
|
163c546236 | ||
|
|
656bc9344b | ||
|
|
b79a8587fa | ||
|
|
17337974a2 | ||
|
|
6bcf477532 | ||
|
|
bf7e8a6ec6 | ||
|
|
f31e8b43b6 | ||
|
|
0090811510 | ||
|
|
b97aeda53c | ||
|
|
f82232194c | ||
|
|
aff5c9a614 | ||
|
|
ddefb1a6ca | ||
|
|
4d06fee1e2 | ||
|
|
8febd81ed2 | ||
|
|
e575f01bef | ||
|
|
f25f507945 | ||
|
|
bdbb529ea8 | ||
|
|
0aac942aa1 | ||
|
|
8cc24bc665 | ||
|
|
478ea66678 | ||
|
|
4dfc898587 | ||
|
|
05e77b2aed | ||
|
|
24863b6393 | ||
|
|
3cf17c8bab | ||
|
|
8c38ecf808 | ||
|
|
a196a26ab4 | ||
|
|
8d73571f5b | ||
|
|
d5348dd9d4 | ||
|
|
7e31b956bf | ||
|
|
dbc5d65d90 | ||
|
|
c91d78fe51 | ||
|
|
798d010776 | ||
|
|
70f0b6ea50 | ||
|
|
4e3ef96374 | ||
|
|
c88a14bf43 | ||
|
|
7f5ed58a79 | ||
|
|
89e349146d | ||
|
|
c5cb2ea4cb | ||
|
|
fe9610abe9 | ||
|
|
1bbde4f5e3 | ||
|
|
e90c196598 | ||
|
|
6a2333be64 | ||
|
|
7d6a64d383 | ||
|
|
42dc2915ea | ||
|
|
3cec6b5a90 | ||
|
|
c31e3d6e3f | ||
|
|
6e4c8a6639 | ||
|
|
809c267419 | ||
|
|
5ac27d1002 | ||
|
|
7237fb04a8 | ||
|
|
d1696425fd | ||
|
|
a614fb1ef6 | ||
|
|
aa38dd3b59 | ||
|
|
2ac050501b | ||
|
|
03abe187ce | ||
|
|
a83d0cf7af | ||
|
|
8bb4fbec12 | ||
|
|
a8fb51ba25 | ||
|
|
24efc37737 | ||
|
|
c25678cc28 | ||
|
|
44970b78a1 | ||
|
|
a3bdbf6202 | ||
|
|
f583a4240a | ||
|
|
4054c6923f | ||
|
|
7a1d84a3fb | ||
|
|
58ed7c0093 | ||
|
|
f56e79ed1f | ||
|
|
45c423bbaf | ||
|
|
78f47e5873 | ||
|
|
21d1a56953 | ||
|
|
69a02bcee0 | ||
|
|
d2c0f553fc | ||
|
|
3967aecfdc | ||
|
|
7b8cc0a8b8 | ||
|
|
28c4e89ab5 | ||
|
|
8c42ac9053 | ||
|
|
86987f9e69 | ||
|
|
33a8267d2c | ||
|
|
779a8a8927 | ||
|
|
1888302cee | ||
|
|
34c446909e | ||
|
|
583765a298 | ||
|
|
4b1f0b5775 | ||
|
|
8f81c810e0 | ||
|
|
970be73052 | ||
|
|
e85469cc3c | ||
|
|
4a41ee8f75 | ||
|
|
9591a5a2b0 | ||
|
|
8f510f401e | ||
|
|
92678c3dae | ||
|
|
9f2d1f90d0 | ||
|
|
23e331610d | ||
|
|
59cb6f9c9c | ||
|
|
98325495ea | ||
|
|
576518b2c8 | ||
|
|
0631bafbbf | ||
|
|
d9b6284914 | ||
|
|
111b927033 | ||
|
|
d63337f06f | ||
|
|
7012e6a66a | ||
|
|
726e1a2268 | ||
|
|
6e455a1205 | ||
|
|
da07d550d2 | ||
|
|
1829a634e9 | ||
|
|
95f66b8c4b | ||
|
|
ea1f43bec7 | ||
|
|
c877d0b964 | ||
|
|
caf09ac644 | ||
|
|
17f151cd5a | ||
|
|
0940158d01 | ||
|
|
e6bbe8351d | ||
|
|
031bae2564 | ||
|
|
b8d790caab | ||
|
|
267557eb90 | ||
|
|
b2af93dfec | ||
|
|
7e81618769 | ||
|
|
eeac5ce998 | ||
|
|
fcf2fd1455 | ||
|
|
fb9dce0386 | ||
|
|
4c09a70647 | ||
|
|
eee59abfa1 | ||
|
|
c7effbccb4 | ||
|
|
2912e4fea6 | ||
|
|
2d09a214ae | ||
|
|
a88c085278 | ||
|
|
5dcca3e088 | ||
|
|
413a1685aa | ||
|
|
7081a7ec3c | ||
|
|
33143ea28e | ||
|
|
f8c86f3b72 | ||
|
|
ae53cc736b | ||
|
|
eca75d1365 | ||
|
|
ef201e6fcf | ||
|
|
38a556b7d6 | ||
|
|
c071746768 | ||
|
|
57512ba48b | ||
|
|
dcf765efda | ||
|
|
525d41e63c | ||
|
|
7d7ca0de4a | ||
|
|
11e5726ea9 | ||
|
|
d3f0c52474 | ||
|
|
8523f835dc | ||
|
|
63ccfac599 | ||
|
|
283baa4a3f | ||
|
|
c7bd132e2f | ||
|
|
3c92e186d6 | ||
|
|
67d8919f8a | ||
|
|
06c0b28d37 | ||
|
|
dfedb45254 | ||
|
|
b373965510 | ||
|
|
52d887f3b4 | ||
|
|
a6ca480210 | ||
|
|
16e80424e0 | ||
|
|
6c72d3adbe | ||
|
|
bbc9c9fb9b | ||
|
|
42d0c4c040 | ||
|
|
8f99886d62 | ||
|
|
60c01e583f | ||
|
|
f21f89f561 | ||
|
|
5f900a3b3c | ||
|
|
60db55b122 | ||
|
|
d9c05a9333 | ||
|
|
ab66ad83fd | ||
|
|
3498fceb6a | ||
|
|
e841774978 | ||
|
|
f297044203 | ||
|
|
c7e803a94c | ||
|
|
ac198d5b5a | ||
|
|
6eb66de94e | ||
|
|
f545b4d002 | ||
|
|
eee3e8b63c | ||
|
|
69afee61e0 | ||
|
|
724f67d381 | ||
|
|
8d69af4445 | ||
|
|
3c301ce742 | ||
|
|
f87c432744 | ||
|
|
d446ba9c1b | ||
|
|
dc669f5cd4 | ||
|
|
d4719d5707 | ||
|
|
f9082dad94 | ||
|
|
a8d421c9cc | ||
|
|
7b13c12ab4 | ||
|
|
97f4adbc1a | ||
|
|
602ab44375 | ||
|
|
77e6fb8225 | ||
|
|
2c8a030ecb | ||
|
|
ccd01e6da5 | ||
|
|
df10cfe641 | ||
|
|
09d3e8aabb | ||
|
|
f17411916f | ||
|
|
75583f67c5 | ||
|
|
bb745b0318 | ||
|
|
3834aeb73f | ||
|
|
aa09c079f6 | ||
|
|
3c586caba4 | ||
|
|
b05740048c | ||
|
|
2b5ed21207 | ||
|
|
acecb88efb | ||
|
|
734c373f3d | ||
|
|
e49cb524a8 | ||
|
|
cc24228511 | ||
|
|
27106fea57 | ||
|
|
990aaadc16 | ||
|
|
0c572b5947 | ||
|
|
3e7c294989 | ||
|
|
8a02156ac0 | ||
|
|
c4a75de0d8 | ||
|
|
db03bcdf8f | ||
|
|
dd5825c770 | ||
|
|
8fbe8f9f2a | ||
|
|
3bc3d19f40 | ||
|
|
575fe8e350 | ||
|
|
d5218a86f6 | ||
|
|
080f93f6de | ||
|
|
df931d95a3 | ||
|
|
d5406d5b50 | ||
|
|
f7e877ce60 | ||
|
|
ad4a1ada45 | ||
|
|
da0d0d21e3 | ||
|
|
7a1d2ca205 | ||
|
|
d89e785440 | ||
|
|
2423b5a4c4 | ||
|
|
1bee877b24 | ||
|
|
4d70ac0894 | ||
|
|
593d9e40f6 | ||
|
|
9fd1a3c663 | ||
|
|
8a781d241c | ||
|
|
be4d610de1 | ||
|
|
736d3460d9 | ||
|
|
f844d70275 | ||
|
|
0c9cfefcd0 | ||
|
|
8ed187b0f5 | ||
|
|
e5bada81dc | ||
|
|
655f817033 | ||
|
|
ebff2ac9f2 | ||
|
|
5a7ffcf499 | ||
|
|
f8e117a7b7 | ||
|
|
c4054f0ac9 | ||
|
|
f7707141d6 | ||
|
|
c40c42fc10 | ||
|
|
6cad976078 | ||
|
|
c82f17e5d2 | ||
|
|
1054bde7fd | ||
|
|
9e23b91f3f | ||
|
|
ea6e064e42 | ||
|
|
cf0858f5b9 | ||
|
|
69b11a8412 | ||
|
|
55aad4254c | ||
|
|
73e1659378 | ||
|
|
98f3e8159e | ||
|
|
e8cff194fc | ||
|
|
f1de1b3b91 | ||
|
|
a134ab3012 | ||
|
|
5a84d7233b | ||
|
|
8fe0112568 | ||
|
|
3acaac5403 | ||
|
|
7dbac5a565 | ||
|
|
8fb4f4063f | ||
|
|
808718fb26 | ||
|
|
6dc8de315a | ||
|
|
afb9b08925 | ||
|
|
2bed034e64 | ||
|
|
2328bb6261 | ||
|
|
06357fa3f9 | ||
|
|
83f58288f9 | ||
|
|
b1d5f0f9e8 | ||
|
|
7142e4db37 | ||
|
|
281a079a62 | ||
|
|
0d638e6bad | ||
|
|
43a63feaca | ||
|
|
4aa6ffe28c | ||
|
|
2ce688ee2a | ||
|
|
4e967980a3 | ||
|
|
93edf07da8 | ||
|
|
a684bead92 | ||
|
|
dd06de2632 | ||
|
|
975f07df99 | ||
|
|
5fe6845d7c | ||
|
|
4aa844946d | ||
|
|
3220e3de31 | ||
|
|
26bba3c5f5 | ||
|
|
c82a5c38df | ||
|
|
9cfaf6eefc | ||
|
|
b881bec668 | ||
|
|
e6f3c63675 | ||
|
|
f2afece658 | ||
|
|
b69ceeeee8 | ||
|
|
b93caa7410 | ||
|
|
7476879bde | ||
|
|
ce884732f3 | ||
|
|
455932b032 | ||
|
|
68a9b4ccae | ||
|
|
f6c205baf9 | ||
|
|
738d10409f | ||
|
|
89c5a3e0a9 | ||
|
|
5dac9d956c | ||
|
|
c19c723795 | ||
|
|
788cd9d6fb | ||
|
|
824d41ef0f | ||
|
|
329b5d9b9e | ||
|
|
b55c2ec55c | ||
|
|
d99e5a44f5 | ||
|
|
e8d5e42300 | ||
|
|
c0afd42fa2 | ||
|
|
1bb6a2ac0d | ||
|
|
3d2f554be9 | ||
|
|
a673e6d138 | ||
|
|
f9b6e92745 | ||
|
|
6229455d25 | ||
|
|
9d6726227a | ||
|
|
64b4efef17 | ||
|
|
eb71601cd6 | ||
|
|
0297e09c17 | ||
|
|
8f6447b67e | ||
|
|
4ae4ce477f | ||
|
|
7bf5deb4c1 | ||
|
|
2fbd9893bd | ||
|
|
5a01c1c5eb | ||
|
|
9a85b661b0 | ||
|
|
d4d981909b | ||
|
|
16035131bc | ||
|
|
b2adb8f058 | ||
|
|
780cb80c8c | ||
|
|
2a8a8f6e6a | ||
|
|
850bda9669 | ||
|
|
25e3395580 | ||
|
|
71dba04d83 | ||
|
|
2a3312e779 | ||
|
|
c4484dcc54 | ||
|
|
61883b13ef | ||
|
|
a220af4fad | ||
|
|
945c122dda | ||
|
|
61b66e88d5 | ||
|
|
9dfd5f3012 | ||
|
|
ff634e32db | ||
|
|
3b583ebd56 | ||
|
|
1a9b410540 | ||
|
|
d7a439477c | ||
|
|
c7f3da237c | ||
|
|
da144de57b | ||
|
|
58d38682eb | ||
|
|
402579f7e2 | ||
|
|
ebf373e680 | ||
|
|
ffe347cfdc | ||
|
|
b572b979a1 | ||
|
|
de49aa0bd4 | ||
|
|
286daa9bec | ||
|
|
65beb6e460 | ||
|
|
63b6af2ac7 | ||
|
|
bdbe132311 | ||
|
|
492bcbfdaa | ||
|
|
46600bf4fc | ||
|
|
aed456bf32 | ||
|
|
d3e807583a | ||
|
|
262f957218 | ||
|
|
8454eef6e9 | ||
|
|
892479d9b9 | ||
|
|
5e24f650af | ||
|
|
de38f1f6fd | ||
|
|
a3e8f45552 | ||
|
|
cd8624ae2d | ||
|
|
7b731a24d1 | ||
|
|
f9bd3d39f0 | ||
|
|
dbc926b0b8 | ||
|
|
0f1a3dfb34 | ||
|
|
cbb9285fb3 | ||
|
|
d7412aab45 | ||
|
|
d8ca29509f | ||
|
|
619cad9c35 | ||
|
|
bca723bcf3 | ||
|
|
ef39f23d1f | ||
|
|
c066e2c3cf | ||
|
|
45af291e6a | ||
|
|
a84184852e | ||
|
|
6cdb872bb5 | ||
|
|
27b76aefd2 | ||
|
|
8820e34870 | ||
|
|
4def4b0341 | ||
|
|
ec0c0eb810 | ||
|
|
5ca498d0f3 | ||
|
|
a894194b6b | ||
|
|
2e8a5d0d86 | ||
|
|
a374deaf30 | ||
|
|
bc29587c55 | ||
|
|
9cb149fa32 | ||
|
|
412c4af7b0 | ||
|
|
28ff0bdfc4 | ||
|
|
f9250cda1a | ||
|
|
b821b9b038 | ||
|
|
6f35f9cbbb | ||
|
|
23d9cd81c8 | ||
|
|
dc63edf0c7 | ||
|
|
ab6ee52b43 | ||
|
|
3121d907c1 | ||
|
|
b09a35f272 | ||
|
|
f21eda2b40 | ||
|
|
ef4370c213 | ||
|
|
9f03e978dd | ||
|
|
54190a9cd2 | ||
|
|
f43ccd4c4e | ||
|
|
20fe04de38 | ||
|
|
dc16f4cebf | ||
|
|
bfaf764f13 | ||
|
|
62d9fb1365 | ||
|
|
9f2b5330d5 | ||
|
|
9a552a7cc4 | ||
|
|
08998a3d17 | ||
|
|
b41f43f4bf | ||
|
|
90740146ff | ||
|
|
778eb54890 | ||
|
|
5196d2fed0 | ||
|
|
5e969aa41a | ||
|
|
1ac03eeb91 | ||
|
|
261e6e3307 | ||
|
|
e38f8b6133 | ||
|
|
bc4204243c | ||
|
|
f93ea0e95c | ||
|
|
b90e15207f | ||
|
|
19da16229c | ||
|
|
73c29660d6 | ||
|
|
6549a3a023 | ||
|
|
4d30fb57d8 | ||
|
|
022e6a2f89 | ||
|
|
da613df71c | ||
|
|
66882b3c25 | ||
|
|
9285764e31 | ||
|
|
835f55e563 | ||
|
|
ef1bd7502f | ||
|
|
320d3c196d | ||
|
|
9d88a4659f | ||
|
|
dae33bab5e | ||
|
|
c0a305babf | ||
|
|
4ffbcea819 | ||
|
|
6808ab496a | ||
|
|
40d43331e4 | ||
|
|
674c276b57 | ||
|
|
5fdc501836 | ||
|
|
c3b1b469b2 | ||
|
|
c30a81acbe | ||
|
|
035acb343e | ||
|
|
53b19d576c | ||
|
|
7efb8abd97 | ||
|
|
b314acf5b5 | ||
|
|
b222dd15c0 | ||
|
|
01212920ae | ||
|
|
4b2d736f0e | ||
|
|
57d5b17a84 | ||
|
|
791d0f5787 | ||
|
|
fa9d604fa3 | ||
|
|
bdb55e4b21 | ||
|
|
e2d707b158 | ||
|
|
7963681585 | ||
|
|
b0158d9388 | ||
|
|
0e86c4aae8 | ||
|
|
2f838f0b6a | ||
|
|
362d48ab81 | ||
|
|
8d7b3beb09 | ||
|
|
6d360a8b03 | ||
|
|
bf53a905a2 | ||
|
|
290cdd2692 | ||
|
|
00570a21f5 | ||
|
|
9a98422076 | ||
|
|
ab9cfee7b1 | ||
|
|
ae59c4f996 | ||
|
|
514fc908b9 | ||
|
|
59107496b4 | ||
|
|
8db090ae9c | ||
|
|
730076469e | ||
|
|
6241b23d4f | ||
|
|
e65872d5df | ||
|
|
eaa38b7676 | ||
|
|
f76fe9efc1 | ||
|
|
6b2ad8826b | ||
|
|
8324d4c4c2 | ||
|
|
2821d797a9 | ||
|
|
367ca399c8 | ||
|
|
d86b01ba33 | ||
|
|
0aa3b288a0 | ||
|
|
589996b79c | ||
|
|
49104a6add | ||
|
|
8051dc5110 | ||
|
|
fecedfdc68 | ||
|
|
f0c82f21d2 | ||
|
|
9f71fcf255 | ||
|
|
c91b28ee92 | ||
|
|
113b3728b1 | ||
|
|
cf193a71b2 | ||
|
|
2d37d4bf9d | ||
|
|
a768575fea | ||
|
|
51ff565c5f | ||
|
|
c5f5f43ccb | ||
|
|
df7b5db47b | ||
|
|
98121e0e64 | ||
|
|
e74f734546 | ||
|
|
d836b75dc4 | ||
|
|
47321aebc4 | ||
|
|
2a0287b358 | ||
|
|
f5fb60aa56 | ||
|
|
a412e37a32 | ||
|
|
dcf147cdfb | ||
|
|
f9e5d9d0a9 | ||
|
|
57a229655c | ||
|
|
a92a2b7291 | ||
|
|
d9fe5bf388 | ||
|
|
6dadb1bf6f | ||
|
|
b7cf5ca174 | ||
|
|
77d5d8ebd4 | ||
|
|
92e62069d4 | ||
|
|
32938eeab7 | ||
|
|
6531770e48 | ||
|
|
561be2e5e8 | ||
|
|
b87298bad9 | ||
|
|
c3df075d91 | ||
|
|
f82e403180 | ||
|
|
afd81a7814 | ||
|
|
ca7f0ad4a6 | ||
|
|
4fc4390173 | ||
|
|
fff54f021c | ||
|
|
6fee16fe5e | ||
|
|
5f43db2e36 | ||
|
|
561bdb3938 | ||
|
|
47576358a2 | ||
|
|
582947accd | ||
|
|
30f4b58ced | ||
|
|
227a271bea | ||
|
|
5455d2997f | ||
|
|
241bb511ea | ||
|
|
85e19bf16c | ||
|
|
e93dd982b9 | ||
|
|
535b52df55 | ||
|
|
a04782b63e | ||
|
|
feb1e53a4d | ||
|
|
705c001681 | ||
|
|
a973fc981a | ||
|
|
77cbe2b545 | ||
|
|
0b5ef6a5e1 | ||
|
|
97f813a777 | ||
|
|
a242588d95 | ||
|
|
b2f5e72bf1 | ||
|
|
0daf8dcd8d | ||
|
|
79e886cfb9 | ||
|
|
4f6d04a3ae | ||
|
|
0e26d5ba5a | ||
|
|
915e0f8483 | ||
|
|
4058d9fbc7 | ||
|
|
6c6ff42879 | ||
|
|
450f74b387 | ||
|
|
f684994868 | ||
|
|
be4240623a | ||
|
|
4b5e003f33 | ||
|
|
6c7c0eb7e7 | ||
|
|
1ba6480110 | ||
|
|
af31962b2d | ||
|
|
f134bd459e | ||
|
|
01d02f8c2e | ||
|
|
092bed6d9d | ||
|
|
8d9b8aced2 | ||
|
|
2856703acc | ||
|
|
14b8a72551 | ||
|
|
0125dcdd1d | ||
|
|
6a15d30579 | ||
|
|
3dcefa3ea1 | ||
|
|
b3a995f880 | ||
|
|
d40be45e9a | ||
|
|
ec952d88cc | ||
|
|
3983dfe004 | ||
|
|
e1c4f8445c | ||
|
|
2a68e0565e | ||
|
|
f4b6173da9 | ||
|
|
63c95df81c | ||
|
|
065f9c0a53 | ||
|
|
abe28937ca | ||
|
|
77b7e03869 | ||
|
|
d5af1bd9a2 | ||
|
|
86f313ec52 | ||
|
|
2e85e130c8 | ||
|
|
d70a87b299 | ||
|
|
c4576564e5 | ||
|
|
f82a7a148b | ||
|
|
b4431d823c | ||
|
|
0168aa3a61 | ||
|
|
2064b14015 | ||
|
|
aeea83d5f5 | ||
|
|
221bf0cefd | ||
|
|
2899f62d95 | ||
|
|
442ca34eb1 | ||
|
|
0202a17138 | ||
|
|
37645ec663 | ||
|
|
b7aed2a85d | ||
|
|
a47c5b5568 | ||
|
|
56eb4a8dc3 | ||
|
|
7b029c890a | ||
|
|
644b4232ca | ||
|
|
ff27e6744e | ||
|
|
fb929e71fc | ||
|
|
0bd7bd1621 | ||
|
|
5cad6e40c4 | ||
|
|
3a4984c1ce | ||
|
|
caa5a648df | ||
|
|
8487e4fb52 | ||
|
|
b508689b53 | ||
|
|
3b4d1bf72a | ||
|
|
0bbbdfa092 | ||
|
|
c1e769ade2 | ||
|
|
a20ce69028 | ||
|
|
80519cb397 | ||
|
|
7347263dfd | ||
|
|
2a3b6b830b | ||
|
|
11b299e116 | ||
|
|
27eef4ce42 | ||
|
|
59badc0137 | ||
|
|
f49698efa1 | ||
|
|
19d6cae10d | ||
|
|
7057f5a5c2 | ||
|
|
3e336204df | ||
|
|
3f4ecb195c | ||
|
|
58b528d00a | ||
|
|
2d2060088c | ||
|
|
d1667fac73 | ||
|
|
98c0b1f9bd | ||
|
|
1d2548a3e6 | ||
|
|
3690959be4 | ||
|
|
7de5fd1515 | ||
|
|
f9d1e39b7b | ||
|
|
16a0d9707b | ||
|
|
6962abfd10 | ||
|
|
dc0d4f0011 | ||
|
|
6bcb535d3f | ||
|
|
4ad9902601 | ||
|
|
2a933788fd | ||
|
|
3febf3e357 | ||
|
|
a8ca80c23c | ||
|
|
f3b1bb742a | ||
|
|
4d1ed0be27 | ||
|
|
af4b9f57f5 | ||
|
|
6e7bd2585f | ||
|
|
40ccdfd9b3 | ||
|
|
659b0ba889 | ||
|
|
71b8699f47 | ||
|
|
24c5bbb182 | ||
|
|
2eea20b161 | ||
|
|
f2180576de | ||
|
|
d9039569bd | ||
|
|
4a82d18cc6 | ||
|
|
f8fa78bb8b | ||
|
|
babfa9aae3 | ||
|
|
ebf2f54cd5 | ||
|
|
20c1e8ca05 | ||
|
|
2a35d51d45 | ||
|
|
ebe2d2ddab | ||
|
|
1ee30e9b53 | ||
|
|
7527003711 | ||
|
|
acd0bbc94f | ||
|
|
00e3f331b4 | ||
|
|
7a6fbecac4 | ||
|
|
6cc746dc83 | ||
|
|
6c3d8dbe64 | ||
|
|
938e3b2b07 | ||
|
|
b4e57438ed | ||
|
|
7bd188dafb | ||
|
|
60724897cc | ||
|
|
e03defc30f | ||
|
|
3bb1f893af | ||
|
|
37a61f527c | ||
|
|
5e3d546fb6 | ||
|
|
b7b5f031f3 | ||
|
|
e57e548c31 | ||
|
|
b0d1d5a07a | ||
|
|
c63380427e | ||
|
|
102e75ce95 | ||
|
|
420c39337c | ||
|
|
214c796a4c | ||
|
|
8918d6bec0 | ||
|
|
ca7acb8339 | ||
|
|
5083ccb605 | ||
|
|
6908aa532c | ||
|
|
a7daa077ac | ||
|
|
9f0d4905b1 | ||
|
|
89d10210be | ||
|
|
545d387bb4 | ||
|
|
e2d27db828 | ||
|
|
33bcac189f | ||
|
|
361e99006b | ||
|
|
7162ab1631 | ||
|
|
9374e0fe18 | ||
|
|
b13ae62d0f | ||
|
|
49de289a9c | ||
|
|
b6dcf72268 | ||
|
|
76cfd185de | ||
|
|
79820a0f05 | ||
|
|
a0126f6a15 | ||
|
|
abd378e5f6 | ||
|
|
e7e3d612a1 | ||
|
|
208dbfd951 | ||
|
|
26e4a05276 | ||
|
|
3e261fb353 | ||
|
|
4775c73aee | ||
|
|
1ece97d0a1 | ||
|
|
87ef8d1977 | ||
|
|
2ebb1728ee | ||
|
|
621b11ebd6 | ||
|
|
12d58f3af2 | ||
|
|
211e815d9c | ||
|
|
465fd2ec0a | ||
|
|
d0c405ae46 | ||
|
|
953d831d5f | ||
|
|
5573db2bc1 | ||
|
|
195b23248b | ||
|
|
83897293c6 | ||
|
|
f26ddef244 | ||
|
|
d25e8e9798 | ||
|
|
bfbd9a8f22 | ||
|
|
bd17f9f5e1 | ||
|
|
8491b86c17 | ||
|
|
376a3743c1 | ||
|
|
a42af5e0d5 | ||
|
|
e157649571 | ||
|
|
e50d1a10d0 | ||
|
|
d474d49ce8 | ||
|
|
4dba4ef641 | ||
|
|
be08fa3bfa | ||
|
|
945b151712 | ||
|
|
2af6486f73 | ||
|
|
9cffc8781a | ||
|
|
b75c1f7f08 | ||
|
|
c5d22bf9e3 | ||
|
|
1baae5e709 | ||
|
|
da3239cfa1 | ||
|
|
ba0078c51c | ||
|
|
47f64401a7 | ||
|
|
8bdbe7c9b7 | ||
|
|
0637018cca | ||
|
|
8a7bef673b | ||
|
|
a0e71ac396 | ||
|
|
184a804367 | ||
|
|
c234b4ea91 | ||
|
|
db13f5e4f3 | ||
|
|
f9a8b3c827 | ||
|
|
17886d0e43 | ||
|
|
1f112d587f | ||
|
|
5c56ea6b22 | ||
|
|
3c76dfbbb3 | ||
|
|
e158e3e426 | ||
|
|
12dc1626a7 | ||
|
|
297e56f4e1 | ||
|
|
09f75441ba | ||
|
|
41bd69d050 | ||
|
|
73b3402d85 | ||
|
|
d66a304b00 | ||
|
|
ee63b247cd | ||
|
|
418e0e2aa3 | ||
|
|
d4bd706fe2 | ||
|
|
a4dfc09c71 | ||
|
|
9ed39f149b | ||
|
|
0e85aa56da | ||
|
|
2f59919f84 | ||
|
|
10baf43ede | ||
|
|
996d7fc90d | ||
|
|
c0febf2fd1 | ||
|
|
f841f65a1e | ||
|
|
c9786ee3f6 | ||
|
|
99b62edcbd | ||
|
|
c588d4139e | ||
|
|
aff55351ad | ||
|
|
96ba075698 | ||
|
|
a7d5415f64 | ||
|
|
dede22c915 | ||
|
|
fbf3fd9d8c | ||
|
|
e70de80cdf | ||
|
|
ef9ec5b262 | ||
|
|
f9b8152f21 | ||
|
|
e0ff8b4320 | ||
|
|
6a699ba51b | ||
|
|
fbcfc7a582 | ||
|
|
1899fd3813 | ||
|
|
bd7c99f94f | ||
|
|
6ba9e50da7 | ||
|
|
21a2d9e82f | ||
|
|
0f20cdaae1 | ||
|
|
5d813b6e43 | ||
|
|
a842acfdb4 | ||
|
|
1e65804a1b | ||
|
|
acf0b082b4 | ||
|
|
2c6305bcd4 | ||
|
|
bd153a0c87 | ||
|
|
fb6987e91a | ||
|
|
74c036483a | ||
|
|
65e10bc20d | ||
|
|
89a1e69bec | ||
|
|
564884797d | ||
|
|
dd1ee56648 | ||
|
|
c54c3754ef | ||
|
|
d72a5075b9 | ||
|
|
6dde5fc6f1 | ||
|
|
880ef63720 | ||
|
|
b75150e91e | ||
|
|
006e21379f | ||
|
|
03850fb31c | ||
|
|
7df0fb456b | ||
|
|
1fe1b5fc4d | ||
|
|
95d179835c | ||
|
|
7c52f504e5 | ||
|
|
44bbfe3ba6 | ||
|
|
57258a9cd3 | ||
|
|
cd25150056 | ||
|
|
c9d50c412d | ||
|
|
732f891850 | ||
|
|
8ef260972d | ||
|
|
bdd1481024 | ||
|
|
810ad46446 | ||
|
|
94cd3d008c | ||
|
|
2ec0b4674c | ||
|
|
5b30c390fd | ||
|
|
9de9c57dc2 | ||
|
|
02dc61b7c7 | ||
|
|
7822f59fd4 | ||
|
|
158f2159b7 | ||
|
|
44c4b29ea2 | ||
|
|
7c13561a4f | ||
|
|
de30ab99ef | ||
|
|
380dab1461 | ||
|
|
de6c651b60 | ||
|
|
91002933e3 | ||
|
|
cd75581ccb | ||
|
|
49d5f560a7 | ||
|
|
409170f661 | ||
|
|
e3f6d4e9fd | ||
|
|
346189cf4c | ||
|
|
b466937d68 | ||
|
|
d338b5ca37 | ||
|
|
9740b65fe7 | ||
|
|
27e2d0baa5 | ||
|
|
4039221b4b | ||
|
|
5bb58429b3 | ||
|
|
204d246a8c | ||
|
|
e0f49ca8f5 | ||
|
|
07cdb37deb | ||
|
|
2764eb9669 | ||
|
|
5c3182e168 | ||
|
|
338ff63153 | ||
|
|
31263b7b22 | ||
|
|
925328c43b | ||
|
|
a36b93a473 | ||
|
|
14cf3c1093 | ||
|
|
534808038e | ||
|
|
83c7d38d42 | ||
|
|
964bfef6e7 | ||
|
|
657fb97d58 | ||
|
|
2b36e8c68b | ||
|
|
ed101e30fa | ||
|
|
9a3bd51664 | ||
|
|
6576fa5ca0 | ||
|
|
45fc2dd07a | ||
|
|
e7a6ddb4ff | ||
|
|
f719813c52 | ||
|
|
9c4adef0c2 | ||
|
|
9f831b2c40 | ||
|
|
dfa057d979 | ||
|
|
7ffac651b6 | ||
|
|
98ab237bf7 | ||
|
|
63e4bcebef | ||
|
|
593a72a967 | ||
|
|
6c8f38a241 | ||
|
|
a910f1442e | ||
|
|
df14afb55f | ||
|
|
732a2d7742 | ||
|
|
78f4cf3155 | ||
|
|
8f763d655d | ||
|
|
64329c3fac | ||
|
|
c8c22a787e | ||
|
|
6a6ec4300b | ||
|
|
44ba5aa568 | ||
|
|
01658c33fd | ||
|
|
efafd4cb3e | ||
|
|
d25740ed51 | ||
|
|
3bb4ad86ff | ||
|
|
b169c96f1c | ||
|
|
843f4b8e28 | ||
|
|
e5b75abc76 | ||
|
|
83777540d0 | ||
|
|
0e28348e16 | ||
|
|
7b7bee2901 | ||
|
|
2a2f703abc | ||
|
|
e85f4e4129 | ||
|
|
bcad6dbe22 | ||
|
|
e9f88a78d5 | ||
|
|
f601ed3806 | ||
|
|
085d26f1b2 | ||
|
|
559546d333 | ||
|
|
da8d92b78e | ||
|
|
de5498c1b2 | ||
|
|
2134bf898a | ||
|
|
e27ed7e79d | ||
|
|
efad5b20e8 | ||
|
|
f27d5ba7d1 | ||
|
|
09a67871fb | ||
|
|
cec9994add | ||
|
|
d45d0018d2 | ||
|
|
410cbd082c | ||
|
|
fd875c41c7 | ||
|
|
750798d0a3 | ||
|
|
04faaea10d | ||
|
|
74831c9b7f | ||
|
|
5471a218eb | ||
|
|
79f2a8dde9 | ||
|
|
5c0b7487f7 | ||
|
|
c021f5ebdc | ||
|
|
3a3baf3c85 | ||
|
|
b8d320c434 | ||
|
|
515e482886 | ||
|
|
ea0805b017 | ||
|
|
ec0e90e8ce | ||
|
|
d4de54f292 | ||
|
|
9124a26a45 | ||
|
|
18603ad24f | ||
|
|
70ac0587db | ||
|
|
230b1bb3db | ||
|
|
23ef884e9b | ||
|
|
bb24b1dfcc | ||
|
|
324e614902 | ||
|
|
20c8d07a46 | ||
|
|
fe9f4939d5 | ||
|
|
a89131c043 | ||
|
|
1f6bb6839a | ||
|
|
1e70717554 | ||
|
|
3cf378e045 | ||
|
|
12ad4420aa | ||
|
|
70000f9df1 | ||
|
|
eaff11ef6e | ||
|
|
6c3b546648 | ||
|
|
384f4f74e0 | ||
|
|
898bac5b04 | ||
|
|
cd08ad693f | ||
|
|
c2577d1d25 | ||
|
|
13075460ac | ||
|
|
cca0848e6d | ||
|
|
150658d58d | ||
|
|
6be1a77a29 | ||
|
|
fb683e438d | ||
|
|
2196a89ec3 | ||
|
|
bafeceeb6e | ||
|
|
eeafad7cd9 | ||
|
|
94b7353fbf | ||
|
|
60cd91f144 | ||
|
|
1d199f8713 | ||
|
|
42b79e5bc6 | ||
|
|
7f12cb3fdc | ||
|
|
6cad1a3ead | ||
|
|
9d5fa55d5c | ||
|
|
4f910e942f | ||
|
|
c43e5827c8 | ||
|
|
58b6702071 | ||
|
|
a6dd3d8354 | ||
|
|
028d17f149 | ||
|
|
1cba4a1a9b | ||
|
|
7243e4a0e2 | ||
|
|
760b00b85f | ||
|
|
fc6760717b | ||
|
|
83c417ed2e | ||
|
|
fcfa763890 | ||
|
|
942b7ef923 | ||
|
|
bc61f79b3a | ||
|
|
d649d7eb1d | ||
|
|
cf20728711 | ||
|
|
cd31f998dd | ||
|
|
51c3215137 | ||
|
|
3e0cb0ed37 | ||
|
|
7d5469ed1c | ||
|
|
d6b081255c | ||
|
|
f9b59d8549 | ||
|
|
1534c26050 | ||
|
|
81eb849aff | ||
|
|
f72ec17c5f | ||
|
|
9f9b933607 | ||
|
|
e5d4369203 | ||
|
|
e1ec38446d | ||
|
|
2d102c4810 | ||
|
|
e59d0f520a | ||
|
|
2063ac15ee | ||
|
|
7b597e0223 | ||
|
|
716f983e71 | ||
|
|
31feb58e1f | ||
|
|
92ef0a60fc | ||
|
|
e21ab12e4c | ||
|
|
b5a0c6505a | ||
|
|
dfb4e20219 | ||
|
|
9cbcd43fda | ||
|
|
7cadbcc533 | ||
|
|
d3a3a7353a | ||
|
|
b8365e9f6e | ||
|
|
e17dd4b5fa | ||
|
|
8dcab568bd | ||
|
|
a0020804c9 | ||
|
|
ea2126a301 | ||
|
|
8442a9a711 | ||
|
|
c07b3de43d | ||
|
|
4f81f402f5 | ||
|
|
10d21a4a0f | ||
|
|
42bcae0e4a | ||
|
|
04c5b2aa36 | ||
|
|
b30f7264f1 | ||
|
|
2800a50f19 | ||
|
|
287ecf5ce2 | ||
|
|
53b7969753 | ||
|
|
9530f17194 | ||
|
|
0829d5bc7d | ||
|
|
b59712ee10 | ||
|
|
e247e45f96 | ||
|
|
df71d93dd9 | ||
|
|
928f10b420 | ||
|
|
a1807fd0c3 | ||
|
|
d937ce4982 | ||
|
|
6411556a97 | ||
|
|
2c334570c3 | ||
|
|
c1671abaa4 | ||
|
|
d82cc98b75 | ||
|
|
5d7af03228 | ||
|
|
529368e858 | ||
|
|
fdc061b7ee | ||
|
|
923b23871f | ||
|
|
409a103990 | ||
|
|
6a6815d893 | ||
|
|
37737e7941 | ||
|
|
af0bc09d52 | ||
|
|
a8d791e2d3 | ||
|
|
c874a734fd | ||
|
|
23e5cefdf1 | ||
|
|
9226b67ab7 | ||
|
|
ee25585f06 | ||
|
|
028c3f9aec | ||
|
|
cc048c41d8 | ||
|
|
ef39a76371 | ||
|
|
1329f1f535 | ||
|
|
66f38e8ecd | ||
|
|
e45b41f55a | ||
|
|
995ff52cf3 | ||
|
|
6e1f66ad94 | ||
|
|
3934e231fe | ||
|
|
960eaa5724 | ||
|
|
37f6ac0c87 | ||
|
|
fcc36ddc83 | ||
|
|
79fdd07d8f | ||
|
|
32ce033c06 | ||
|
|
abb194ca9c | ||
|
|
3161cb0322 | ||
|
|
f36cadb809 | ||
|
|
863e42691a | ||
|
|
762dce7853 | ||
|
|
f0d190d157 | ||
|
|
6103d3b8bd | ||
|
|
7a89f303db | ||
|
|
b05d9a0a75 | ||
|
|
e89503e1fa | ||
|
|
e0cd1aba29 | ||
|
|
a5f985257c | ||
|
|
90efe14bfb | ||
|
|
fc9ae9ca20 | ||
|
|
649297df83 | ||
|
|
ae7e7578db | ||
|
|
0cf6605b8d | ||
|
|
4a52620d83 | ||
|
|
251570b638 | ||
|
|
7660a694cb | ||
|
|
7e7b268a66 | ||
|
|
660c1777e3 | ||
|
|
5407df03fa | ||
|
|
84c34361a0 | ||
|
|
50cf5e5c7a | ||
|
|
aba737c61b | ||
|
|
9bbbe9e7c1 | ||
|
|
163f494b8a | ||
|
|
7f7eb78d8c | ||
|
|
462d25bebc | ||
|
|
2bf8caf7fc | ||
|
|
c6a74b66ce | ||
|
|
d22619c1f9 | ||
|
|
d8b59a18ed | ||
|
|
fae6bdff05 | ||
|
|
37182f5138 | ||
|
|
70e8ab8349 | ||
|
|
102244f467 | ||
|
|
db946b93ec | ||
|
|
5b637577c8 | ||
|
|
0258051f06 | ||
|
|
2ab22882d6 | ||
|
|
55f6241769 | ||
|
|
9ec3325cd0 | ||
|
|
e0ab901600 | ||
|
|
fcaab30fe7 | ||
|
|
e8ecd9b2e0 | ||
|
|
22da5a1ff0 | ||
|
|
6d0c38371a | ||
|
|
b10cf4bebb | ||
|
|
6e9871ba4e | ||
|
|
13b23d9ec9 | ||
|
|
1491f29f96 | ||
|
|
9f4b6d5f43 | ||
|
|
a883514c8a | ||
|
|
9ee6d7fc91 | ||
|
|
7df1a19da4 | ||
|
|
2ee6c8487d | ||
|
|
de3dbb8c1e | ||
|
|
f851423b68 | ||
|
|
8ec2b35557 | ||
|
|
ae4b233458 | ||
|
|
6872d57581 | ||
|
|
79962a7566 | ||
|
|
d4c6282455 | ||
|
|
db3370cd21 | ||
|
|
4213e3163a | ||
|
|
3850bbb68e | ||
|
|
ea95050d43 | ||
|
|
c32dba1ecb | ||
|
|
94a5020faf | ||
|
|
c0e6bf1299 | ||
|
|
9fe8958e8c | ||
|
|
7c51895b0f | ||
|
|
16bca85438 | ||
|
|
9d415d0dbe | ||
|
|
1d0210a372 | ||
|
|
2fb6c08702 | ||
|
|
f7c712f6eb | ||
|
|
eabd25ee6a | ||
|
|
5fc49ab3c2 | ||
|
|
1b6a722c0c | ||
|
|
6414cd52c0 | ||
|
|
f732ed970b | ||
|
|
5cfffcfa83 | ||
|
|
4e28f7bb4e | ||
|
|
532d50ad7a | ||
|
|
d8b0d338c0 | ||
|
|
cd0be5f79d | ||
|
|
3820894454 | ||
|
|
9a60ab07a8 | ||
|
|
12b91e7671 | ||
|
|
fe79ee0315 | ||
|
|
9c3b3e698e | ||
|
|
ca4eeb332a | ||
|
|
794c3efb7d | ||
|
|
20fe9c45cf | ||
|
|
26da872704 | ||
|
|
8a1d5d3a48 | ||
|
|
0392a2a343 | ||
|
|
34a2f3b32b | ||
|
|
4bb8914d9a | ||
|
|
f7c80a0101 | ||
|
|
2d417b4a37 | ||
|
|
46bb400ffd | ||
|
|
8b05d75f97 | ||
|
|
4861d35628 | ||
|
|
4a8bfcf647 | ||
|
|
b311f0e091 | ||
|
|
4e4399b727 | ||
|
|
bbab9c1a6b | ||
|
|
5938836ff5 | ||
|
|
d003790e4d | ||
|
|
1d0fa9a5f6 | ||
|
|
8fe192267d | ||
|
|
92255797d9 | ||
|
|
f404285140 | ||
|
|
ee38c717a5 | ||
|
|
1668392296 | ||
|
|
c1feaecbcb | ||
|
|
dea1c74fcc | ||
|
|
22a0f2c14e | ||
|
|
94c34eeb23 | ||
|
|
d63d976916 | ||
|
|
54601db44a | ||
|
|
4701decfcd | ||
|
|
cc81c8ff4c | ||
|
|
8febc78d0e | ||
|
|
87838bd4ce | ||
|
|
dfc354550c | ||
|
|
ef36466b3b | ||
|
|
7041039572 | ||
|
|
97b4e19777 | ||
|
|
55bf6e86f7 | ||
|
|
683ec662b5 | ||
|
|
87c5844704 | ||
|
|
094a0ea76d | ||
|
|
ebc086106f | ||
|
|
6384f5538c | ||
|
|
befedfd80a | ||
|
|
d828a92ea3 | ||
|
|
4711b28c25 | ||
|
|
d7a90e6be4 | ||
|
|
267e770c63 | ||
|
|
b233f18a0f | ||
|
|
32092d212e | ||
|
|
b7b52eee80 | ||
|
|
a8da7e60c3 | ||
|
|
83e944c985 | ||
|
|
7f841d49b2 | ||
|
|
bdc52204e4 | ||
|
|
c95b080267 | ||
|
|
11cdcc65ad | ||
|
|
1e9ec9e053 | ||
|
|
7bafc54280 | ||
|
|
69d5aef59b | ||
|
|
985e61b3c6 | ||
|
|
15f512a3c7 | ||
|
|
1f3f7b4560 | ||
|
|
fecd63e582 | ||
|
|
a32613c854 | ||
|
|
38e55367b1 | ||
|
|
73b837f4d9 | ||
|
|
9b971aa124 | ||
|
|
8479198268 | ||
|
|
9daaf5bb6a | ||
|
|
4e99ff1c39 | ||
|
|
8e8458e557 | ||
|
|
391ac51f0f | ||
|
|
4bc8fb207a | ||
|
|
42cd36afb7 | ||
|
|
dd0436e68e | ||
|
|
d76a0d9f22 | ||
|
|
d9e047e20e | ||
|
|
af35ff7419 | ||
|
|
81bace1dca | ||
|
|
38c69de01b | ||
|
|
213ca07c38 | ||
|
|
012fa91e83 | ||
|
|
3af1182206 | ||
|
|
04e00bb834 | ||
|
|
df089cb0a5 | ||
|
|
56aa1b39f0 | ||
|
|
d940ab36e1 | ||
|
|
0f48d51062 | ||
|
|
1c5344ba6e | ||
|
|
0d4654122c | ||
|
|
63a9d58c67 | ||
|
|
5a397afd06 | ||
|
|
b8109401d1 | ||
|
|
5e09c80b71 | ||
|
|
b906f88a44 | ||
|
|
24b1b53ba0 | ||
|
|
53fae9fbbd | ||
|
|
ad4ed7a06b | ||
|
|
7f5e655730 | ||
|
|
e96a9f0b46 | ||
|
|
db7b4fa937 | ||
|
|
7112341c51 | ||
|
|
f4d60f963d | ||
|
|
c6babc7dc4 | ||
|
|
3905ed796e | ||
|
|
595d006d5b | ||
|
|
3bcf6d7ca0 | ||
|
|
68f5ee7bde | ||
|
|
9db6e2161b | ||
|
|
6eeb75a35e | ||
|
|
1f717617b0 | ||
|
|
3d7231929c | ||
|
|
3b4668cc19 | ||
|
|
34ad3fcfe8 | ||
|
|
242224396d | ||
|
|
68b3cb8a34 | ||
|
|
1cfeda8fe5 | ||
|
|
33af2d37b3 | ||
|
|
69505974fe | ||
|
|
a77dd9a11f | ||
|
|
1bc017eac9 | ||
|
|
07dec2e641 | ||
|
|
d86a839265 | ||
|
|
72d8a26ede | ||
|
|
cae4dd81c9 | ||
|
|
eba68c56ef | ||
|
|
99516f5a75 | ||
|
|
37a2e89c81 | ||
|
|
ed837fbf22 | ||
|
|
913b29070f | ||
|
|
e5ddd57d65 | ||
|
|
db35ec682a | ||
|
|
111889565a | ||
|
|
c68ece96cd | ||
|
|
0741881959 | ||
|
|
f7745928ab | ||
|
|
976ee35a35 | ||
|
|
c51e254287 | ||
|
|
9feea66550 | ||
|
|
ee7bd5fb8a | ||
|
|
fff5b3d85a | ||
|
|
d706d0eb22 | ||
|
|
944ce80c1e | ||
|
|
d8ef8cb12f | ||
|
|
eea7bed2f3 | ||
|
|
741bcd1a80 | ||
|
|
9c9cfd015d | ||
|
|
39a1b0742f | ||
|
|
03bcf573c3 | ||
|
|
97d1b4fafa | ||
|
|
d79d3fd4dc | ||
|
|
f60993b042 | ||
|
|
1005126a5f | ||
|
|
fa647a915c | ||
|
|
2cf0b9d097 | ||
|
|
7d68a2967f | ||
|
|
b96be69a5c | ||
|
|
636cd8cd50 | ||
|
|
0536d0abcb | ||
|
|
c647f852d6 | ||
|
|
ebab879aca | ||
|
|
7561635b24 | ||
|
|
7ed819e84a | ||
|
|
407a83e81d | ||
|
|
3c3731252d | ||
|
|
8b64328087 | ||
|
|
f1bb5b3d1d | ||
|
|
7aeab47df4 | ||
|
|
e1b848afd0 | ||
|
|
f111ddf449 | ||
|
|
534c827904 | ||
|
|
efa765a9fc | ||
|
|
910f5693d8 | ||
|
|
7c5ed7e8f8 | ||
|
|
066626d928 | ||
|
|
6bb8c9c271 | ||
|
|
e76850fce6 | ||
|
|
8d3503e0fc | ||
|
|
9788af4273 | ||
|
|
4442200d6b | ||
|
|
02b12f370a | ||
|
|
dce6349fcb | ||
|
|
862c79ee16 | ||
|
|
29c1d74202 | ||
|
|
d83679c895 | ||
|
|
62bdbed1d5 | ||
|
|
390a2b573f | ||
|
|
bbbd7ab41c | ||
|
|
85eb82a175 | ||
|
|
82189d35e0 | ||
|
|
f880e86cd1 | ||
|
|
87f30a3633 | ||
|
|
47b029a871 | ||
|
|
b16e2cad19 | ||
|
|
f8b347a03a | ||
|
|
12da3a58fc | ||
|
|
73d7ee37f8 | ||
|
|
7fdee0ebe5 | ||
|
|
9437b35c8c | ||
|
|
78326629f3 | ||
|
|
db4c3b70bb | ||
|
|
09d937f533 | ||
|
|
d3615e8d2b | ||
|
|
aeaf325e2a | ||
|
|
d629dc24ae | ||
|
|
d8c356e350 | ||
|
|
55d2dcca04 | ||
|
|
9bd9503e9b | ||
|
|
56495522b8 | ||
|
|
9d1d5c439b | ||
|
|
be5e419288 | ||
|
|
8afe6c5228 | ||
|
|
20b46a33cf | ||
|
|
1694b4b3a6 | ||
|
|
5b1b78d386 | ||
|
|
9c249596c0 | ||
|
|
c8273cec2c | ||
|
|
36394520ff | ||
|
|
45adaf5dc2 | ||
|
|
be0680675b | ||
|
|
6c466d13ff | ||
|
|
9905b20448 | ||
|
|
2ff410946a | ||
|
|
94be7e5294 | ||
|
|
da3939239f | ||
|
|
adefbb3365 | ||
|
|
8000a51918 | ||
|
|
7012c6e77a | ||
|
|
13843772d4 | ||
|
|
ad4cd2067b | ||
|
|
ff3efd7d56 | ||
|
|
8d6a406779 | ||
|
|
98d15e2e34 | ||
|
|
d5c591317b | ||
|
|
817afb13d1 | ||
|
|
98f29f945b | ||
|
|
09fdf5b990 | ||
|
|
b760aa8c63 | ||
|
|
a83e6b6929 | ||
|
|
64fa18220f | ||
|
|
b11a766c28 | ||
|
|
c2c3993887 | ||
|
|
35459b7332 | ||
|
|
1b8e37a62c | ||
|
|
b117e7e2bb | ||
|
|
64fd2304a9 | ||
|
|
6b521e9985 | ||
|
|
2e19bf5b17 | ||
|
|
69f41a0200 | ||
|
|
3cbf741209 | ||
|
|
9d2473bbe3 | ||
|
|
89cc9ad27d | ||
|
|
d8a5dc586d | ||
|
|
837b2f7558 | ||
|
|
1df7df21d5 | ||
|
|
53a40de2e7 | ||
|
|
940cafacac | ||
|
|
d1ba4a1759 | ||
|
|
6437e2ec67 | ||
|
|
31e3b9953f | ||
|
|
f5bdb8b15b | ||
|
|
fbcf312071 | ||
|
|
9a7a8a3243 | ||
|
|
fc36950c1d | ||
|
|
b4ca44f096 | ||
|
|
4978dd86ac | ||
|
|
730b29c9cc | ||
|
|
4d631d1b6a | ||
|
|
2d2629c088 | ||
|
|
9a52edacb2 | ||
|
|
67411e32ff | ||
|
|
e06d3200c3 | ||
|
|
9a3eb3e0fd | ||
|
|
a7c96acc81 | ||
|
|
f2ab33b498 | ||
|
|
6a7c9e34a0 | ||
|
|
6c8b3d2f8f | ||
|
|
c0943a7c58 | ||
|
|
5d230c444c | ||
|
|
ac615b4a25 | ||
|
|
bc45b50290 | ||
|
|
1bbd84b37a | ||
|
|
aa3fb4807b | ||
|
|
d1a4057a8d | ||
|
|
8519d2724b | ||
|
|
ba36a47228 | ||
|
|
e0d8dc0334 | ||
|
|
8dec4814a9 | ||
|
|
6167562758 | ||
|
|
7e68f5270d | ||
|
|
a24b7d4c8f | ||
|
|
25aa967146 | ||
|
|
3bfc7d3d23 | ||
|
|
430a3e3fc9 | ||
|
|
c94d782037 | ||
|
|
233f9698f3 | ||
|
|
61fc15cec0 | ||
|
|
4e754e0d86 | ||
|
|
125ab7d95e | ||
|
|
26c5ff1f93 | ||
|
|
ffa3a96f1a | ||
|
|
9818d8bb6c | ||
|
|
f7fad736c3 | ||
|
|
62cc99c1c9 | ||
|
|
44424583f0 | ||
|
|
98cb0878d9 | ||
|
|
5aa63d35ce | ||
|
|
11099c88dc | ||
|
|
ef22c46199 | ||
|
|
e8cd6856b5 | ||
|
|
3d36802686 | ||
|
|
dc706aeb43 | ||
|
|
071b6816e3 | ||
|
|
cfdff61d08 | ||
|
|
2132960d7c | ||
|
|
fefb0b23af | ||
|
|
c1da1a8a16 | ||
|
|
93bdbb1c50 | ||
|
|
f1e421db05 | ||
|
|
462ba62656 | ||
|
|
5cd073c96f | ||
|
|
40b8d865a9 | ||
|
|
a21d7db390 | ||
|
|
cc61a89c68 | ||
|
|
3316c2ded3 | ||
|
|
b6989ac82a | ||
|
|
6bf06116df | ||
|
|
5bb5bc42ee | ||
|
|
57e10a8d2b | ||
|
|
51fade6bd3 | ||
|
|
48ffe5e660 | ||
|
|
22fdc3d1bf | ||
|
|
04b65c7c0d | ||
|
|
3576eb8081 | ||
|
|
9377b73aa3 | ||
|
|
e5aff3f366 | ||
|
|
78356ab298 | ||
|
|
947a367865 | ||
|
|
e79d9ec2f9 | ||
|
|
16e8451166 | ||
|
|
1d54a8dccd | ||
|
|
b68d2d9115 | ||
|
|
64d540f23b | ||
|
|
d8d681e8bc | ||
|
|
5b9f608667 | ||
|
|
7660046720 | ||
|
|
0315b32d2b | ||
|
|
5f906e54e4 | ||
|
|
143f0ea67b | ||
|
|
0aa2cffb5e | ||
|
|
f2a7953d9d | ||
|
|
f231dc13cf | ||
|
|
a107ee67fa | ||
|
|
cb488cbde8 | ||
|
|
18d3da66f3 | ||
|
|
61dd92129a | ||
|
|
489c0f3108 | ||
|
|
e327580a2f | ||
|
|
bd9c28e29c | ||
|
|
f4d7148f66 | ||
|
|
1dd5bc8f14 | ||
|
|
59db640d0d | ||
|
|
4bb35f5fab | ||
|
|
967a0c31fd | ||
|
|
c5c8dd7ad7 | ||
|
|
d3e2707fce | ||
|
|
4cba4c7a1f | ||
|
|
3b1fd05940 | ||
|
|
5bc5c0ae86 | ||
|
|
5fc801f8a6 | ||
|
|
f7a23c094c | ||
|
|
516b1f765e | ||
|
|
d28c915635 | ||
|
|
f76606bc26 | ||
|
|
7ba3394508 | ||
|
|
f19eeff899 | ||
|
|
d3c9da6d5f | ||
|
|
1ce908177e | ||
|
|
609bf13765 | ||
|
|
97a49fab2f | ||
|
|
10ead27676 | ||
|
|
8be7ea5cc1 | ||
|
|
ebefba9e32 | ||
|
|
fb784d6a91 | ||
|
|
c31639ebbd | ||
|
|
4ff8d6fbc3 | ||
|
|
d029f81992 | ||
|
|
6b7c2675f1 | ||
|
|
4f8c184bc0 | ||
|
|
afc608fc5d | ||
|
|
8523875349 | ||
|
|
79955c7fac | ||
|
|
d3cbd70054 | ||
|
|
81706b8726 | ||
|
|
2812a54210 | ||
|
|
258d768887 | ||
|
|
1059066c05 | ||
|
|
875f3c07b3 | ||
|
|
8ce72ea842 | ||
|
|
e542d38ec7 | ||
|
|
771eaf97c8 | ||
|
|
b40ed13f47 | ||
|
|
b9de49d5ab | ||
|
|
ead6fa5f1f | ||
|
|
6ada8ba6a2 | ||
|
|
672b19b106 | ||
|
|
4a2580c9ea | ||
|
|
52c8c9341a | ||
|
|
72c4a7abd6 | ||
|
|
d022a1fa5e | ||
|
|
a142620b70 | ||
|
|
5603336253 | ||
|
|
838a3464c1 | ||
|
|
0906ae3c93 | ||
|
|
af8ed99ae7 | ||
|
|
f8d1e159f4 | ||
|
|
df999e040c | ||
|
|
2e13bc42a1 | ||
|
|
9fd2519c12 | ||
|
|
325bdfe92f | ||
|
|
9211fa065b | ||
|
|
bb170ee208 | ||
|
|
8333b39928 | ||
|
|
fefff3b788 | ||
|
|
d5e985fde5 | ||
|
|
ed27f980c2 | ||
|
|
b6afad1787 | ||
|
|
7807ea5f8c | ||
|
|
d463d35906 | ||
|
|
5cde325d9a | ||
|
|
b038763b7b | ||
|
|
aa448a8c2e | ||
|
|
a830eb4ea0 | ||
|
|
73397ab500 | ||
|
|
a7f6eafd5c | ||
|
|
ed9083de24 | ||
|
|
a2fa92abf1 | ||
|
|
fa1e1fd779 | ||
|
|
adde66bc57 | ||
|
|
db08fc3da2 | ||
|
|
97e603b215 | ||
|
|
29936d76b1 | ||
|
|
3f88aaae64 | ||
|
|
fccf46c67d | ||
|
|
970aca1c9d | ||
|
|
5e494f0982 | ||
|
|
91a7dc8cf0 | ||
|
|
e52cd28f1e | ||
|
|
7bdbd4cb03 | ||
|
|
95d694f6c5 | ||
|
|
313fc75ec8 | ||
|
|
639a69a639 | ||
|
|
a4ec3ad6da | ||
|
|
b7c10c95d3 | ||
|
|
67d2d2fe95 | ||
|
|
0aa8d63a6e | ||
|
|
7b11cdcb74 | ||
|
|
071a5a4bdf | ||
|
|
5ede6c3021 | ||
|
|
7ff7b0c2d1 | ||
|
|
30dab7df9f | ||
|
|
afff06c7e6 | ||
|
|
05ef43c342 | ||
|
|
c235754df2 | ||
|
|
70dab149ba | ||
|
|
ed4b44a78a | ||
|
|
54f113ab5f | ||
|
|
60bf81d950 | ||
|
|
bdfd58f468 | ||
|
|
de1aaf3808 | ||
|
|
769aee1107 | ||
|
|
3663e6d12a | ||
|
|
a6d9984453 | ||
|
|
4e8cf136c8 | ||
|
|
f49d7478d7 | ||
|
|
b4a1c9d648 | ||
|
|
692246ec44 | ||
|
|
48a7d28aa6 | ||
|
|
04146f897d | ||
|
|
a34658c97f | ||
|
|
cbfeb0158e | ||
|
|
8d37e00869 | ||
|
|
584d6ae9cf | ||
|
|
421e611356 | ||
|
|
f078713d28 | ||
|
|
a8d31d52cf | ||
|
|
d9213b2fe2 | ||
|
|
091efe52fc | ||
|
|
a5c508733a | ||
|
|
ce944d9a7d | ||
|
|
8321b5adba | ||
|
|
667c972308 | ||
|
|
3dbe05be3a | ||
|
|
2d4ce19250 | ||
|
|
3b3e0c0acd | ||
|
|
c3ddb933bb | ||
|
|
6aae60ece7 | ||
|
|
6b3dc8ece0 | ||
|
|
7dd231a8c9 | ||
|
|
35a3219012 | ||
|
|
7598e6ab4b | ||
|
|
bbedc5f41b | ||
|
|
e1a2c45b19 | ||
|
|
8fa801e032 | ||
|
|
75870dc6c1 | ||
|
|
42900b5d0e | ||
|
|
c1423d77ff | ||
|
|
0e43c54214 | ||
|
|
2cc4161239 | ||
|
|
fc8f3fdf27 | ||
|
|
24b76208ac | ||
|
|
0de272b195 | ||
|
|
7faf40004c | ||
|
|
88ae2d473a | ||
|
|
337b47685c | ||
|
|
ed3f9be655 | ||
|
|
248c6d5f22 | ||
|
|
dd4aa09d21 | ||
|
|
132f2226ca | ||
|
|
2eb6e95fed | ||
|
|
db8130be4f | ||
|
|
379b649e95 | ||
|
|
e5619492ef | ||
|
|
cc76ccc626 | ||
|
|
3d6512dd11 | ||
|
|
b19d97e01f | ||
|
|
25931a618b | ||
|
|
ffab576399 | ||
|
|
e7067ab9cf | ||
|
|
7cfcf6d579 | ||
|
|
a4b586055a | ||
|
|
5ad1e1b645 | ||
|
|
01ed513a79 | ||
|
|
504f68b8aa | ||
|
|
cbb08f5642 | ||
|
|
a4b5d681ce | ||
|
|
02f93f3a14 | ||
|
|
ad2f4573f8 | ||
|
|
06604cd738 | ||
|
|
089be35b5d | ||
|
|
bbcb335d60 | ||
|
|
6ef2e0bb5f | ||
|
|
d8202d881d | ||
|
|
aae814a156 | ||
|
|
49bcc5368d | ||
|
|
555e04f9e7 | ||
|
|
3f6f2e4e23 | ||
|
|
abffc4b067 | ||
|
|
363cb0b679 | ||
|
|
d26910ba9c | ||
|
|
74b2f305ea | ||
|
|
6c2f893651 | ||
|
|
faaf121eb6 | ||
|
|
83ab65163d | ||
|
|
9dcd5ff332 | ||
|
|
c6635f63c1 | ||
|
|
56213219e4 | ||
|
|
7c2dc20dbe | ||
|
|
c8e8317ea4 | ||
|
|
8509796743 | ||
|
|
90aaed0f2c | ||
|
|
48be15b742 | ||
|
|
a95b3f2f99 | ||
|
|
b2cc7ab84f | ||
|
|
eb3414f07f | ||
|
|
292dad130d | ||
|
|
ec41cddb19 | ||
|
|
5871f8290d | ||
|
|
33089be2cd | ||
|
|
d351c8d14c | ||
|
|
82446e5ffa | ||
|
|
b786164e8a | ||
|
|
f9cbb3aac8 | ||
|
|
a66c19c6c7 | ||
|
|
94d1667d70 | ||
|
|
3399db1cff | ||
|
|
874ea99d19 | ||
|
|
7022fb87b4 | ||
|
|
7c1e2a6af0 | ||
|
|
2f011c3266 | ||
|
|
4762aa0897 | ||
|
|
f30f83331f | ||
|
|
3695e1e3e5 | ||
|
|
585b5929aa | ||
|
|
0185cdf785 | ||
|
|
8d22ca66ba | ||
|
|
b0eacb2a79 | ||
|
|
9b40370794 | ||
|
|
95f3d58383 | ||
|
|
0f0cde1093 | ||
|
|
e679366dac | ||
|
|
ca56df5cfe | ||
|
|
d8a4209768 | ||
|
|
40712a2e62 | ||
|
|
acb9ce33b1 | ||
|
|
5e43a7145a | ||
|
|
39bd6694f2 | ||
|
|
5de8c4f9c3 | ||
|
|
31a554d94f | ||
|
|
9bc9fc46ff | ||
|
|
4274e06795 | ||
|
|
a2bf235553 | ||
|
|
1b18b2b188 | ||
|
|
9c27447b17 | ||
|
|
f03d98cd0d | ||
|
|
6331bebb30 | ||
|
|
fdd4c4aaa0 | ||
|
|
4dd404771e | ||
|
|
bf267e9c95 | ||
|
|
843f70cdba | ||
|
|
42e0e07c14 | ||
|
|
dfdd2b9043 | ||
|
|
8656fcd8d1 | ||
|
|
f2f6b9d49c | ||
|
|
82f1e6753b | ||
|
|
7ed717607a | ||
|
|
0ec9491d21 | ||
|
|
416970c819 | ||
|
|
ccc28f3617 | ||
|
|
5bac36b30f | ||
|
|
ef3ffddec7 | ||
|
|
e6ba467d98 | ||
|
|
314508bcd8 | ||
|
|
da18506e41 | ||
|
|
5eaee0b71e | ||
|
|
bd93e7dc7e | ||
|
|
2c762813ba | ||
|
|
136c6fa70b | ||
|
|
67b2343571 | ||
|
|
3caa1d9c4a | ||
|
|
b0c924ca03 | ||
|
|
f6f59023b4 | ||
|
|
9dc4e7c955 | ||
|
|
faa44e54ae | ||
|
|
bfb743b851 | ||
|
|
dad2e92dd3 | ||
|
|
59c312ea40 | ||
|
|
48c5a458f3 | ||
|
|
c0830862c8 | ||
|
|
42deb7abbe | ||
|
|
62deda6470 | ||
|
|
3e0981978a | ||
|
|
35e5170907 | ||
|
|
8eba5dcc01 | ||
|
|
5c2248d419 | ||
|
|
335d91b42d | ||
|
|
102b11b1b5 | ||
|
|
26df09b13f | ||
|
|
9259d04771 | ||
|
|
254991c56c | ||
|
|
a492ab0143 | ||
|
|
b0d63b2ec0 | ||
|
|
85b0c63eb0 | ||
|
|
98a92f51e6 | ||
|
|
ae50dbd47c | ||
|
|
a97e628520 | ||
|
|
b48dcc1418 | ||
|
|
8867cdbc02 | ||
|
|
f03ee4b836 | ||
|
|
90418b204e | ||
|
|
918674e01a | ||
|
|
7b44b7d559 | ||
|
|
612b11cbe8 | ||
|
|
b34f05690c | ||
|
|
9b01a05727 | ||
|
|
d0024409df | ||
|
|
91856372f0 | ||
|
|
b1fb838b19 | ||
|
|
2937b6a804 | ||
|
|
b76a8249fa | ||
|
|
db09476137 | ||
|
|
08a5e57180 | ||
|
|
7464d827fe | ||
|
|
ae0ec159e1 | ||
|
|
2a1c08da65 | ||
|
|
faab174a79 | ||
|
|
521b441da5 | ||
|
|
59ca00b33b | ||
|
|
6564ed69d8 | ||
|
|
fadd9032c6 | ||
|
|
80918f5b9b | ||
|
|
e061b3e631 | ||
|
|
06ec3f80b9 | ||
|
|
e6011287f4 | ||
|
|
a0f560ca1a | ||
|
|
7c7d606aa7 | ||
|
|
46587e3cf1 | ||
|
|
603ef4044c | ||
|
|
2e3abfb2cd | ||
|
|
47ccb7ded8 | ||
|
|
98907a886c | ||
|
|
7e14247ea9 | ||
|
|
e103427750 | ||
|
|
95f55b00b3 | ||
|
|
e519984790 | ||
|
|
fa3223777f | ||
|
|
eeb4966294 | ||
|
|
5823859b2a | ||
|
|
7b21bd26d0 | ||
|
|
4ac224688c | ||
|
|
4742e7f64f | ||
|
|
a66f127828 | ||
|
|
e84d88b7a3 | ||
|
|
cda2616a8a | ||
|
|
11aa4d12bd | ||
|
|
18dbeea003 | ||
|
|
3e916c6054 | ||
|
|
fc420c2c0f | ||
|
|
7b9d653c46 | ||
|
|
140441b777 | ||
|
|
5db0e9c8d8 | ||
|
|
63d1c19263 | ||
|
|
018cd25593 | ||
|
|
963737d3fb | ||
|
|
c059f44bf1 | ||
|
|
890f0d1ef6 | ||
|
|
5fca005a3f | ||
|
|
86d4f8e219 | ||
|
|
6b0ab45e63 | ||
|
|
0b475ab5e2 | ||
|
|
15bf74f770 | ||
|
|
32986e3ebd | ||
|
|
f106f27df4 | ||
|
|
041cd40ec2 | ||
|
|
8721f56269 | ||
|
|
1d3045c799 | ||
|
|
52a1ed869c | ||
|
|
04f60baec5 | ||
|
|
a8de436424 | ||
|
|
c7780e9f42 | ||
|
|
3edd7b8b01 | ||
|
|
455202cd1a | ||
|
|
8bdb82c7be | ||
|
|
fa503ee66a | ||
|
|
e1a2ee2381 | ||
|
|
b82d26527a | ||
|
|
1c50dd6b48 | ||
|
|
b0e9df1400 | ||
|
|
6ebf51ce45 | ||
|
|
d9a34f3384 | ||
|
|
8136a1e136 | ||
|
|
41f3606572 | ||
|
|
ea0542dcb1 | ||
|
|
a4dbc1bac2 | ||
|
|
6b5d6648de | ||
|
|
95538707c9 | ||
|
|
4c76a921b1 | ||
|
|
85c1c987af | ||
|
|
bde86323fd | ||
|
|
880e3f388d | ||
|
|
c1535b1a12 | ||
|
|
232ff1ba33 | ||
|
|
1b63dcd4e5 | ||
|
|
47c8e828df |
22
.eslintrc.js
Normal file
22
.eslintrc.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
extends: [
|
||||
'standard'
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly'
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
},
|
||||
rules: {
|
||||
"indent": ["error", 4],
|
||||
"semi": ["error", "always"]
|
||||
}
|
||||
}
|
||||
58
.github/workflows/main.yml
vendored
Normal file
58
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: continuous integration
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- 'README**'
|
||||
- 'HOW_TO_RELEASE**'
|
||||
- 'LOGGING**'
|
||||
|
||||
env:
|
||||
GCLOUD_VERSION: '306.0.0'
|
||||
ARTIFACTS_PROJECT_ID: cartodb-on-gcp-main-artifacts
|
||||
|
||||
jobs:
|
||||
build-test-docker:
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.CARTOFANTE_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
# we tag with "latest" but we don't push it on purpose. We use it as a base for the testing image
|
||||
run: |
|
||||
echo ${GITHUB_SHA::7}
|
||||
echo ${GITHUB_REF##*/}
|
||||
docker build -f private/Dockerfile -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:latest -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/} -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_SHA::7} -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/}--${GITHUB_SHA::7} .
|
||||
|
||||
- name: Build testing image
|
||||
# here it uses the lastest from prev step to add the needed parts on top
|
||||
run: |
|
||||
docker build -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft-test:latest -f private/Dockerfile.test .
|
||||
|
||||
- name: Setup gcloud authentication
|
||||
uses: google-github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: ${{env.GCLOUD_VERSION}}
|
||||
service_account_key: ${{ secrets.ARTIFACTS_GCLOUD_ACCOUNT_BASE64 }}
|
||||
|
||||
- name: Configure docker and pull images
|
||||
# we pull images manually, as if done in next step using docker-compose it fails because missing openssl
|
||||
run: |
|
||||
gcloud auth configure-docker
|
||||
docker pull gcr.io/cartodb-on-gcp-main-artifacts/postgres:latest
|
||||
docker pull gcr.io/cartodb-on-gcp-main-artifacts/redis:latest
|
||||
|
||||
- name: Run tests inside container
|
||||
run: docker-compose -f private/ci/docker-compose.yml run windshaft-tests
|
||||
|
||||
- name: Upload image
|
||||
run: |
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/}
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_SHA::7}
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/}--${GITHUB_SHA::7}
|
||||
|
||||
47
.github/workflows/master.yml
vendored
Normal file
47
.github/workflows/master.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# in this workflow we don't run the tests. Only build image, tag (also latests) and upload. The tests are not run because they are run
|
||||
# on each pull request, and there is a branch protection that forces to have branch up to date before merging, so tests are always run
|
||||
# with the latest code
|
||||
|
||||
name: master build image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
GCLOUD_VERSION: '306.0.0'
|
||||
ARTIFACTS_PROJECT_ID: cartodb-on-gcp-main-artifacts
|
||||
|
||||
jobs:
|
||||
build-master:
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.CARTOFANTE_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
echo ${GITHUB_SHA::7}
|
||||
echo ${GITHUB_REF##*/}
|
||||
docker build -f private/Dockerfile -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:latest -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/} -t gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_SHA::7} .
|
||||
|
||||
- name: Setup gcloud authentication
|
||||
uses: google-github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: ${{env.GCLOUD_VERSION}}
|
||||
service_account_key: ${{ secrets.ARTIFACTS_GCLOUD_ACCOUNT_BASE64 }}
|
||||
|
||||
- name: Configure docker
|
||||
run: |
|
||||
gcloud auth configure-docker
|
||||
|
||||
- name: Upload image
|
||||
run: |
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_REF##*/}
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:${GITHUB_SHA::7}
|
||||
docker push gcr.io/$ARTIFACTS_PROJECT_ID/windshaft:latest
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ redis.pid
|
||||
*.log
|
||||
coverage/
|
||||
.DS_Store
|
||||
.nyc_output
|
||||
build_resources/
|
||||
.dockerignore
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "private"]
|
||||
path = private
|
||||
url = git@github.com:CartoDB/Windshaft-cartodb-private.git
|
||||
branch = master
|
||||
95
.jshintrc
95
.jshintrc
@@ -1,95 +0,0 @@
|
||||
{
|
||||
// // JSHint Default Configuration File (as on JSHint website)
|
||||
// // See http://jshint.com/docs/ for more details
|
||||
//
|
||||
// "maxerr" : 50, // {int} Maximum error before stopping
|
||||
//
|
||||
// // 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
|
||||
"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 () { } ());`
|
||||
// "indent" : 4, // {int} Number of spaces to use for indentation
|
||||
// "latedef" : false, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
// "noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
// "plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
// "quotmark" : false, // Quotation mark consistency:
|
||||
// // false : do nothing (default)
|
||||
// // true : ensure whatever is used is consistent
|
||||
// // "single" : require single quotes
|
||||
// // "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
// "strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
// "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" : 6, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : 120, // {int} Max number of characters per line
|
||||
//
|
||||
// // Relaxing
|
||||
// "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
// "boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
// "eqnull" : false, // true: Tolerate use of `== null`
|
||||
// "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// // (ex: `for each`, multiple try/catch, function expression…)
|
||||
// "evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
// "expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
// "funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
// "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
// "iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
// "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
// "laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
// "laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
// "loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
// "multistr" : false, // true: Tolerate multi-line strings
|
||||
// "noyield" : false, // true: Tolerate generator functions with no yield statement in them.
|
||||
// "notypeof" : false, // true: Tolerate invalid typeof operator values
|
||||
// "proto" : false, // true: Tolerate using the `__proto__` property
|
||||
// "scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
// "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
// "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
// "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
// "validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
//
|
||||
// // Environments
|
||||
// "browser" : true, // Web Browser (window, document, etc)
|
||||
// "browserify" : false, // Browserify (node.js code in the browser)
|
||||
// "couch" : false, // CouchDB
|
||||
// "devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
// "dojo" : false, // Dojo Toolkit
|
||||
// "jasmine" : false, // Jasmine
|
||||
// "jquery" : false, // jQuery
|
||||
// "mocha" : true, // Mocha
|
||||
// "mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
// "nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
// "prototypejs" : false, // Prototype and Scriptaculous
|
||||
// "qunit" : false, // QUnit
|
||||
// "rhino" : false, // Rhino
|
||||
// "shelljs" : false, // ShellJS
|
||||
// "worker" : false, // Web Workers
|
||||
// "wsh" : false, // Windows Scripting Host
|
||||
// "yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom predefined global variables
|
||||
"predef": [
|
||||
"-console", // disallows console, use debug
|
||||
"beforeEach",
|
||||
"afterEach",
|
||||
"before",
|
||||
"after",
|
||||
"describe",
|
||||
"it"
|
||||
]
|
||||
}
|
||||
14
.travis.yml
14
.travis.yml
@@ -1,14 +0,0 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- docker pull cartoimages/windshaft-testing
|
||||
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh
|
||||
|
||||
language: generic
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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://carto.com/contributions).
|
||||
@@ -1,18 +0,0 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. If there are modified dependencies in package.json, update them with `yarn upgrade {{package_name}}@{{version}}`
|
||||
5. Commit package.json, yarn.lock, NEWS
|
||||
6. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
7. Stub NEWS/package for next version
|
||||
|
||||
Versions:
|
||||
|
||||
Bugfix releases increment Patch component of version.
|
||||
Feature releases increment Minor and set Patch to zero.
|
||||
If backward compatibility is broken, increment Major and
|
||||
set to zero Minor and Patch.
|
||||
|
||||
Branches named 'b<Major>.<Minor>' are kept for any critical
|
||||
fix that might need to be shipped before next feature release
|
||||
is ready.
|
||||
16
HOW_TO_RELEASE.md
Normal file
16
HOW_TO_RELEASE.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# How to release
|
||||
|
||||
1. Test (npm test), fix if broken before proceeding.
|
||||
2. Ensure proper version in `package.json` and `package-lock.json`.
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date.
|
||||
4. If there are modified dependencies in `package.json`, update them with `npm upgrade {{package_name}}@{{version}}`.
|
||||
5. Commit `package.json`, `package-lock.json`, NEWS.
|
||||
6. Run `git tag -a Major.Minor.Patch`. Use NEWS section as content.
|
||||
7. Stub NEWS/package for next version.
|
||||
|
||||
## Version:
|
||||
|
||||
* Bugfix releases increment Patch component of version.
|
||||
* Feature releases increment Minor and set Patch to zero.
|
||||
* If backward compatibility is broken, increment Major and set to zero Minor and Patch.
|
||||
* Branches named 'b<Major>.<Minor>' are kept for any critical fix that might need to be shipped before next feature release is ready.
|
||||
53
INSTALL.md
53
INSTALL.md
@@ -1,53 +0,0 @@
|
||||
# Installing Windshaft-CartoDB #
|
||||
|
||||
## Requirements ##
|
||||
Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=6.9.x
|
||||
- yarn >=0.27.5 <1.0.0
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik >3.x. 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:
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
Note that the ```yarn``` 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
|
||||
```yarn``` again.
|
||||
21
LOGGING.md
Normal file
21
LOGGING.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Logging structured traces
|
||||
|
||||
In order to have meaningful and useful log traces, you should follow
|
||||
some general guidelines described in the [Project Guidelines](http://doc-internal.cartodb.net/platform/guidelines.html#structured-logging).
|
||||
|
||||
In this project there is a specific logger in place that takes care of
|
||||
format and context of the traces for you. Take a look at [logger.js](https://github.com/CartoDB/Windshaft-cartodb/blob/cf82e1954e2244861e47fce0c2223ee466a5cd64/lib/utils/logger.js)
|
||||
(NOTE: that file will be moved soon to a common module).
|
||||
|
||||
The logger is instantiated as part of the [app startup process](https://github.com/CartoDB/Windshaft-cartodb/blob/cf82e1954e2244861e47fce0c2223ee466a5cd64/app.js#L53),
|
||||
then passed to middlewares and other client classes.
|
||||
|
||||
There are many examples of how to use the logger to generate traces
|
||||
throughout the code. Here are a few of them:
|
||||
|
||||
```js
|
||||
lib/api/middlewares/logger.js: res.locals.logger.info({ client_request: req }, 'Incoming request');
|
||||
lib/api/middlewares/logger.js: res.on('finish', () => res.locals.logger.info({ server_response: res, status: res.statusCode }, 'Response sent'));
|
||||
lib/api/middlewares/profiler.js: logger.info({ stats, duration: stats.response / 1000, duration_ms: stats.response }, 'Request profiling stats');
|
||||
lib/api/middlewares/tag.js: res.on('finish', () => logger.info({ tags: res.locals.tags }, 'Request tagged'));
|
||||
```
|
||||
53
Makefile
53
Makefile
@@ -1,53 +0,0 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
pre-install:
|
||||
@$(SHELL) ./scripts/check-node-canvas.sh
|
||||
|
||||
all:
|
||||
@$(SHELL) ./scripts/install.sh
|
||||
|
||||
clean:
|
||||
rm -rf node_modules/
|
||||
|
||||
distclean: clean
|
||||
rm config.status*
|
||||
|
||||
config.status--test:
|
||||
./configure --environment=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_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: test jshint
|
||||
|
||||
coverage:
|
||||
@RUNTESTFLAGS=--with-coverage make test
|
||||
|
||||
check: test
|
||||
|
||||
.PHONY: pre-install test jshint coverage
|
||||
319
NEWS.md
319
NEWS.md
@@ -1,5 +1,324 @@
|
||||
# Changelog
|
||||
|
||||
## 10.0.0
|
||||
Released 2020-mm-dd
|
||||
|
||||
Breaking changes:
|
||||
- Log system revamp:
|
||||
- Logs to stdout, disabled while testing
|
||||
- Upgrade `camshaft` to version [`0.67.2`](https://github.com/CartoDB/camshaft/releases/tag/0.67.2)
|
||||
- Use header `X-Request-Id`, or create a new `uuid` when no present, to identyfy log entries
|
||||
- Be able to set log level from env variable `LOG_LEVEL`, useful while testing: `LOG_LEVEL=info npm test`; even more human-readable: `LOG_LEVEL=info npm t | ./node_modules/.bin/pino-pretty`
|
||||
- Stop responding with `X-Tiler-Errors` header. Now errors are properly logged and will end up in ELK as usual.
|
||||
- Stop responding with `X-Tiler-Profiler` header. Now profiling stats are properly logged and will end up in ELK as usual.
|
||||
- Be able to reduce the footprint in the final log file depending on the environment
|
||||
- Be able to pass the logger to the analysis creation (camshaft) while instantiating a named map with analysis.
|
||||
- Be able to tag requests with labels as an easier way to provide business metrics
|
||||
- Metro: Add log-collector utility (`metro`), it will be moved to its own repository. Attaching it here fro development purposes. Try it with the following command `LOG_LEVEL=info npm t | node metro`
|
||||
- Metro: Creates `metrics-collector.js` a stream to update Prometheus' counters and histograms and exposes them via Express' app (`:9145/metrics`). Use the ones defined in `grok_exporter`
|
||||
|
||||
Bug Fixes:
|
||||
- While instantiating a map, set the `cache buster` equal to `0` when there are no affected tables in the MapConfig. Thus `layergroupid` has the same structure always:
|
||||
- `${map_id}:${cache_buster}` for anonymous map
|
||||
- `${user}@${template_hash}@${map_id}:${cache_buster}` for named map
|
||||
|
||||
## 9.0.0
|
||||
Released 2020-06-05
|
||||
|
||||
Breaking changes:
|
||||
- Remove `/version` endpoint
|
||||
- Drop support for Node.js < 12
|
||||
|
||||
Announcements:
|
||||
- Support Node.js 12
|
||||
- Upgrade `windshaft` to version [`7.0.1`](https://github.com/CartoDB/Windshaft/releases/tag/7.0.1)
|
||||
- Upgrade `camshaft` to version [`0.65.3`](https://github.com/CartoDB/camshaft/blob/0.65.3/CHANGELOG.md#0653):
|
||||
- Fix noisy message logs while checking analyses' limits
|
||||
- Fix CI setup, explicit use of PGPORT while creating the PostgreSQL cluster
|
||||
- Upgrade `cartodb-redis` to version [`3.0.0`](https://github.com/CartoDB/node-cartodb-redis/releases/tag/3.0.0)
|
||||
- Fix test where `http-fallback-image` renderer was failing quietly
|
||||
- Fix stat `named map providers` cache count
|
||||
- Use new signature for `onTileErrorStrategy`. Required by `windshaft@6.0.0`
|
||||
- Extract `onTileErrorStrategy` to a module
|
||||
- In tests, stop using mapnik module exposed by windshaft and require it from development dependencies
|
||||
- Stop using `MapStore` from `windshaft` while testing and create a custom one instead
|
||||
- Rename NamedMapProviderReporter by NamedMapProviderCacheReporter
|
||||
- Remove `bootstrapFonts` at process startup (now done in `windshaft@6.0.0`)
|
||||
- Stop checking the installed version of some dependencies while testing
|
||||
- Send metrics about `map views` (#1162)
|
||||
- Add custom headers in responses to allow to other components to be able to get insights about user activity
|
||||
- Update dependencies to avoid security vulnerabilities
|
||||
|
||||
Bug Fixes:
|
||||
- Parsing date column in numeric histograms (#1160)
|
||||
- Use `Array.prototype.sort()`'s callback properly while testing. It should return a number not a boolean.
|
||||
|
||||
## 8.1.1
|
||||
Released 2020-02-17
|
||||
|
||||
Announcements:
|
||||
- Upgrade camshaft to [`0.65.2`](https://github.com/CartoDB/camshaft/blob/69c9447c9fccf00a70a67d713d1ce777775a17ff/CHANGELOG.md#0652): Fixes uncatched errors problem (#1117)
|
||||
|
||||
## 8.1.0
|
||||
Released 2020-01-27
|
||||
|
||||
Announcements:
|
||||
- Removed `jshint` as linter in favour of `eslint` to check syntax, find problems, and enforce code style.
|
||||
- Upgrade `camshaft` to [`0.65.1`](https://github.com/CartoDB/camshaft/blob/a2836c15fd2830f8364a222eeafdb4dc2f41b580/CHANGELOG.md#0651): Use quoted identifiers for column names and enforce the usage of the cartodb schema when using cartodb extension functions and tables.
|
||||
- Stop using two different tools for package management, testing, and any other developer workflow.
|
||||
- Removes Makefile and related bash scripts
|
||||
- Use npm scripts as the only tool for testing, CI and linting.
|
||||
- Simplified CI configuration.
|
||||
- Improved documentation:
|
||||
- Centralized several documents into README.md
|
||||
- Remove outdated sections
|
||||
- Update old sections
|
||||
- Added missing sections.
|
||||
- Remove deprecated coverage tool istanbul, using nyc instead.
|
||||
- Removed unused dockerfiles
|
||||
- Use cartodb schema when using cartodb extension functions and tables.
|
||||
- Implemented circle and polygon dataview filters.
|
||||
|
||||
## 8.0.0
|
||||
Released 2019-11-13
|
||||
|
||||
Breaking changes:
|
||||
- Schema change for "routes" in configuration file, each "router" is now an array instead of an object. See [`dd06de2`](https://github.com/CartoDB/Windshaft-cartodb/pull/1126/commits/dd06de2632661e19d64c9fbc2be0ba1a8059f54c) for more details.
|
||||
|
||||
Announcements:
|
||||
- Added validation to only allow "count" and "sum" aggregations in dataview overview.
|
||||
- Added mechanism to inject custom middlewares through configuration.
|
||||
- Stop requiring unused config properties: "base_url", "base_url_mapconfig", and "base_url_templated".
|
||||
- Upgraded cartodb-query-tables to version [0.7.0](https://github.com/CartoDB/node-cartodb-query-tables/blob/0.7.0/NEWS.md#version-0.7.0).
|
||||
- Be able to set a coherent TTL in Cache-Control header to expire all resources belonging to a map simultaneously.
|
||||
- When `cache buster` in request path is `0` set header `Last-Modified` to now, it avoids stalled content in 3rd party cache providers when they add `If-Modified-Since` header into the request.
|
||||
- Adding a logger to MapStore (#1134)
|
||||
- Qualify calls to cartodb extension so having it in the search_path isn't necessary.
|
||||
- Fix multiple DB login issues.
|
||||
|
||||
## 7.2.0
|
||||
Released 2019-09-30
|
||||
|
||||
Announcements:
|
||||
|
||||
- Stop caching map template errors in Named Map Provider Cache
|
||||
- Gather metrics from Named Maps Providers Cache
|
||||
- Improved efficiency of query samples while instatiating a map (#1120).
|
||||
- Cache control header fine tuning. Set a shorter value for "max-age" directive if there is no way to know when to trigger the invalidation.
|
||||
- Update deps:
|
||||
- Update `cartodb-query-tables` to version [`0.6.3`](https://github.com/CartoDB/node-cartodb-query-tables/blob/0.6.3/NEWS.md#version-063).
|
||||
- Update `cartodb-psql` to [`0.14.0`](https://github.com/CartoDB/node-cartodb-psql/blob/0.14.0/NEWS.md#version-0140-2019-09-10)
|
||||
- Upgrade `windshaft` to [`5.6.3`](https://github.com/CartoDB/Windshaft/blob/master/NEWS.md#version-563):
|
||||
- Upgrade grainstore to [`2.0.1`](https://github.com/CartoDB/grainstore/releases/tag/2.0.1)
|
||||
- Update @carto/mapnik to [`3.6.2-carto.16`](https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto.16/CHANGELOG.carto.md#362-carto16).
|
||||
- Update turbo-carto to [`0.21.2`](https://github.com/CartoDB/turbo-carto/releases/tag/0.21.2)
|
||||
- Upgrade `@carto/cartonik` to version [`0.7.0`](https://github.com/CartoDB/cartonik/blob/v0.7.0/CHANGELOG.md#cartonik-changelog).
|
||||
- Upgrade `camshaft` to [`0.64.2`](https://github.com/CartoDB/camshaft/blob/8b89fcff276da20a71269bed28b7ad6704392898/CHANGELOG.md#0642) to update dependencies.
|
||||
|
||||
## 7.1.0
|
||||
Released 2019-05-06
|
||||
|
||||
Announcements:
|
||||
- Fix uncaught exception: TypeError: Cannot read property 'id' of undefined
|
||||
- Implements graceful shutdown for:
|
||||
- system signals `SIGINT` and `SIGTERM`
|
||||
- events `uncaughtException`, `unhandledRejection` and, `ENOMEM`
|
||||
- Experimental support for listing features in a grid when the map uses the dynamic agregation.
|
||||
- Numeric histogram performance improvement (#1080)
|
||||
- Fix boolean aggregation layer option not working when numbers of rows are above the threshold (#1082)
|
||||
- Update deps:
|
||||
- camshat@0.64.0
|
||||
- windshaft@5.2.0:
|
||||
- Use [`@carto/cartonik`](https://github.com/CartoDB/cartonik/releases/tag/v0.5.0) instead of `@mapbox/tilelive` to fetch raster/vertor tiles.
|
||||
- Upgrade `grainstore` to version `2.0.0`
|
||||
- Upgrade `torque.js` to version `3.1.0`
|
||||
- Upgrade `canvas` to version `2.4.1`
|
||||
- Update @carto/mapnik to [`3.6.2-carto.13`](https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto.13/CHANGELOG.carto.md#362-carto13).
|
||||
|
||||
|
||||
## 7.0.0
|
||||
Released 2019-02-22
|
||||
|
||||
Breaking changes:
|
||||
- Drop support for Node.js 6
|
||||
- Drop support for npm 3
|
||||
- Stop supporting `yarn.lock`
|
||||
- Drop support for Postgres 9.5
|
||||
- Drop support for PosGIS 2.2
|
||||
- Drop support for Redis 3
|
||||
|
||||
Announcements:
|
||||
- In configuration, set `clipByBox2d` to true by default
|
||||
- Update docs: compatible Node.js and npm versions
|
||||
- Report fine-grained Garbage Collector stats
|
||||
- Adding Authorization to Access-Control-Allow-Headers (https://github.com/CartoDB/CartoDB-SQL-API/issues/534)
|
||||
- Update deps:
|
||||
- windshaft@4.13.1: Upgrade tilelive-mapnik to version 0.6.18-cdb18
|
||||
- camshaft@0.63.4: Improve error message for exceeded batch SQL API payload size: add suggestions about what the user can do about it.
|
||||
- Update dev deps:
|
||||
- jshint@2.9.7
|
||||
- mocha@5.2.0
|
||||
- Be able to customize max waiting workers parameter
|
||||
- Handle 'max waitingClients count exceeded' error as "429, You are over platform's limits"
|
||||
|
||||
## 6.5.1
|
||||
Released 2018-12-26
|
||||
|
||||
Bug Fixes:
|
||||
- Update carto-package.json
|
||||
|
||||
## 6.5.0
|
||||
Released 2018-12-26
|
||||
|
||||
New features
|
||||
- Suport Node.js 10
|
||||
- Configure travis to run docker tests against Node.js 6 & 10 versions
|
||||
- Aggregation time dimensions
|
||||
- Update sample configurations to use PostGIS to generate MVT's by default (as in production)
|
||||
- Upgrades Windshaft to [4.12.1](https://github.com/CartoDB/Windshaft/blob/4.12.1/NEWS.md#version-4121)
|
||||
- `pg-mvt`: Use `query-rewriter` to compose the query to render a MVT tile. If not defined, it will use a Default Query Rewriter.
|
||||
- `pg-mvt`: Fix bug while building query and there is no columns defined for the layer.
|
||||
- `pg-mvt`: Accept trailing semicolon in input queries.
|
||||
- `Renderer Cache Entry`: Do not throw errors for integrity checks.
|
||||
- Fix bug when releasing the renderer cache entry in some scenarios.
|
||||
- Upgrade grainstore to [1.10.0](https://github.com/CartoDB/grainstore/releases/tag/1.10.0)
|
||||
- Upgrade cartodb-redis to [2.1.0](https://github.com/CartoDB/node-cartodb-redis/releases/tag/2.1.0)
|
||||
- Upgrade cartodb-query-tables to [0.4.0](https://github.com/CartoDB/node-cartodb-query-tables/releases/tag/0.4.0)
|
||||
- Upgrade cartodb-psql to [0.13.1](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.13.1)
|
||||
- Upgrade turbo-carto to [0.21.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.21.0)
|
||||
- Upgrade camshaft to [0.63.1](https://github.com/CartoDB/camshaft/releases/tag/0.63.1)
|
||||
- Upgrade redis-mpool to [0.7.0](https://github.com/CartoDB/node-redis-mpool/releases/tag/0.7.0)
|
||||
|
||||
Bug Fixes:
|
||||
- Prevent from uncaught exception: Range filter Error from camshaft when getting analysis query.
|
||||
- Make all modules to use strict mode semantics.
|
||||
|
||||
## 6.4.0
|
||||
Released 2018-09-24
|
||||
|
||||
- Upgrades Camshaft to [0.62.3](https://github.com/CartoDB/camshaft/releases/tag/0.61.11):
|
||||
- Build query from node's cache to compute output columns when building analysis
|
||||
- Adds metadata columns for street level geocoding
|
||||
- Remove use of `step` module to handle asynchronous code, now it's defined as development dependency.
|
||||
- Bug Fixes: (#1020)
|
||||
- Fix bug in date-wrapper regarding columns with spaces
|
||||
- Fix bug in aggregation-query regarding columns with spaces
|
||||
- Upgrades Windshaft to [4.10.0](https://github.com/CartoDB/Windshaft/blob/4.10.0/NEWS.md#version-4100)
|
||||
- `pg-mvt`:
|
||||
- Now matches the behaviour of the `mapnik` renderer for MVTs.
|
||||
- Removed undocummented filtering by `layer.options.columns`.
|
||||
- Implement timeout in getTile.
|
||||
- Several bugfixes.
|
||||
- Dependency updates: Fixed a bug in Mapnik MVT renderer and cleanup in `tilelive-mapnik`.
|
||||
- [MapConfig 1.8.0 released](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.8.0.md) with new options for MVTs:
|
||||
- Add **`vector_extent`** option in MapConfig to setup the layer extent.
|
||||
- Add **`vector_simplify_extent`** option in MapConfig to configure the simplification process.
|
||||
- Remove use of `step` module to handle asynchronous code, now it's defined as development dependency.
|
||||
|
||||
## 6.3.0
|
||||
Released 2018-07-26
|
||||
|
||||
- Upgrades Camshaft to [0.62.1](https://github.com/CartoDB/camshaft/releases/tag/0.62.1):
|
||||
- Support for batch street-level geocoding. [0.62.1](https://github.com/CartoDB/camshaft/releases/tag/0.62.1)
|
||||
|
||||
## 6.2.0
|
||||
Released 2018-07-20
|
||||
|
||||
Notice:
|
||||
- This release changes the way that authentication works internally. You'll need to run `bundle exec rake carto:api_key:create_default` in your development environment to keep working.
|
||||
|
||||
New features:
|
||||
- CI tests with Ubuntu Xenial + PostgreSQL 10.1 and Ubuntu Precise + PostgreSQL 9.5
|
||||
- Upgrades Windshaft to [4.8.3](https://github.com/CartoDB/Windshaft/blob/4.8.3/NEWS.md#version-483) which includes:
|
||||
- Update internal deps.
|
||||
- A fix in mapnik-vector-tile to avoid grouping together properties with the same value but a different type.
|
||||
- Performance improvements in the marker symbolizer (local cache, avoid building the collision matrix when possible).
|
||||
- MVT: Disable simplify_distance to avoid multiple simplifications.
|
||||
- Fix a bug with zero length lines not being rendered when using the marker symbolizer.
|
||||
- Reduce size of npm package
|
||||
- Omit attributes validation in layers with aggregation to avoid potentially long instantiation times
|
||||
- Upgrades Camshaft to [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11):
|
||||
- Use Dollar-Quoted String Constants to avoid Syntax Error while running moran analyses. [0.61.10](https://github.com/CartoDB/camshaft/releases/tag/0.61.10)
|
||||
- Quote name columns when performing trade area analysis to avoid Syntax Errors. [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11)
|
||||
- Update other deps:
|
||||
- body-parser: 1.18.3
|
||||
- cartodb-psql: 0.11.0
|
||||
- cartodb-redis: 2.0.1
|
||||
- dot: 1.1.2
|
||||
- express: 4.16.3
|
||||
- lru-cache: 4.1.3
|
||||
- node-statsd: 0.1.1,
|
||||
- queue-async: 1.1.0
|
||||
- request: 2.87.0
|
||||
- semver: 5.5.0
|
||||
- step: 1.0.0
|
||||
- turbo-carto: 0.20.4
|
||||
- yargs: 11.1.0
|
||||
- Update devel deps:
|
||||
- istanbul: 0.4.5
|
||||
- jshint: 2.9.5
|
||||
- mocha: 3.5.3
|
||||
- moment: 2.22.1
|
||||
- nock: 9.2.6
|
||||
- strftime: 0.10.0
|
||||
- Optional instantiation metadata stats (https://github.com/CartoDB/Windshaft-cartodb/pull/952)
|
||||
- Experimental dates_as_numbers support
|
||||
- Tiles base urls with api key
|
||||
|
||||
Bug Fixes:
|
||||
- Validates tile coordinates (z/x/y) from request params to be a valid integer value.
|
||||
- Static maps fails for unsupported formats
|
||||
- Handling errors extracting the column type on dataviews
|
||||
- Fix `meta.stats.estimatedFeatureCount` for aggregations and queries with tokens
|
||||
- Fix numeric histogram bounds when `start` and `end` are specified (#991)
|
||||
- Static maps filters correctly if `layer` option is passed in the url.
|
||||
- Aggregation doesn't return out-of-tile, partially aggregated clusters
|
||||
- Aggregation was not accurate for high zoom, far away from the origin tiles
|
||||
|
||||
Announcements:
|
||||
* Improve error message when the DB query is over the user's limits
|
||||
|
||||
## 6.1.0
|
||||
Released 2018-04-16
|
||||
|
||||
New features:
|
||||
- Aggreation filters
|
||||
- Upgrades Windshaft to 4.7.0, which includes @carto/mapnik v3.6.2-carto.7 with improvements to metrics and markers caching. It also adds an option to disable the markers symbolizer caches in mapnik.
|
||||
|
||||
Bug Fixes:
|
||||
- Non-default aggregation selected the wrong columns (e.g. for vector tiles)
|
||||
- Aggregation dimensions with alias where broken
|
||||
- cartodb_id was not unique accross aggregated vector tiles
|
||||
|
||||
## 6.0.0
|
||||
Released 2018-03-19
|
||||
Backward incompatible changes:
|
||||
- Needs Redis v4
|
||||
|
||||
New features:
|
||||
- Upgrades camshaft to 0.61.8
|
||||
- Upgrades cartodb-redis to 1.0.0
|
||||
- Rate limit feature (disabled by default)
|
||||
- Fixes for tests with PG11
|
||||
|
||||
## 5.4.0
|
||||
Released 2018-03-15
|
||||
- Upgrades Windshaft to 4.5.7 ([Mapnik top metrics](https://github.com/CartoDB/Windshaft/pull/597), [AttributesBackend allows multiple features if all the attributes are the same](https://github.com/CartoDB/Windshaft/pull/602))
|
||||
- Implemented middleware to authorize users via new Api Key system
|
||||
- Keep the old authorization system as fallback
|
||||
- Aggregation widget: Remove NULL categories in 'count' aggregations too
|
||||
- Update request to 2.85.0
|
||||
- Update camshaft to 0.61.4 (Fixes for AOI and Merge analyses)
|
||||
- Update windshaft to 4.6.0, which in turn updates @carto/mapnik to 3.6.2-carto.4 and related dependencies. It brings in a cache for rasterized symbols. See https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto/CHANGELOG.carto.md#362-carto4
|
||||
- PostGIS: Variables in postgis SQL queries must now additionally be wrapped in `!` (refs [#29](https://github.com/CartoDB/mapnik/issues/29), [mapnik/#3618](https://github.com/mapnik/mapnik/pull/3618)):
|
||||
```sql
|
||||
-- Before
|
||||
SELECT ... WHERE trait = @variable
|
||||
|
||||
-- Now
|
||||
SELECT ... WHERE trait = !@variable!
|
||||
```
|
||||
|
||||
## 5.3.1
|
||||
Released 2018-02-13
|
||||
- Improve the speed of the aggregation dataview #865
|
||||
|
||||
204
README.md
204
README.md
@@ -1,82 +1,146 @@
|
||||
Windshaft-CartoDB
|
||||
==================
|
||||
# Windshaft-CartoDB [](https://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
|
||||
[](https://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
The [`CARTO Maps API`](https://carto.com/developers/maps-api/) tiler. It extends [`Windshaft`](https://github.com/CartoDB/Windshaft) and exposes a web service with extra functionality:
|
||||
|
||||
This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api.html) tiler. It extends
|
||||
[Windshaft](https://github.com/CartoDB/Windshaft) with some extra functionality and custom filters for authentication.
|
||||
* Instantiate [`Anonymous Maps`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/03-anonymous-maps.md) through CARTO's map configuration ([`MapConfig`](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-specification.md)).
|
||||
* Create [`Named Maps`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/04-named-maps.md) based on customizable templates.
|
||||
* Get map previews through [`Static Maps`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/05-static-maps-API.md) API.
|
||||
* Render maps with a large amount of data faster using [`Tile Aggregation`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/06-tile-aggregation.md).
|
||||
* Build advanced maps with enriched data through [`Analyses Extension`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/09-MapConfig-analyses-extension.md).
|
||||
* Fetch tabular data from analysis nodes with [`Dataviews`](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/guides/10-MapConfig-dataviews-extension.md)
|
||||
|
||||
* reads dbname from subdomain and cartodb redis for pretty tile urls
|
||||
* configures windshaft to publish `cartodb_id` as the interactivity layer
|
||||
* 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 [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
## Build
|
||||
|
||||
Install
|
||||
-------
|
||||
See [INSTALL.md](INSTALL.md) for detailed installation instructions.
|
||||
Requirements:
|
||||
|
||||
Configure
|
||||
---------
|
||||
* [`Node 12.x `](https://nodejs.org/dist/latest-v10.x/)
|
||||
* [`PostgreSQL >= 11.0`](https://www.postgresql.org/download/)
|
||||
* [`PostGIS >= 2.4`](https://postgis.net/install/)
|
||||
* [`CARTO Postgres Extension >= 0.24.1`](https://github.com/CartoDB/cartodb-postgresql)
|
||||
* [`Redis >= 4`](https://redis.io/download)
|
||||
* `libcairo2-dev`, `libpango1.0-dev`, `libjpeg8-dev` and `libgif-dev` for server side canvas support
|
||||
* `C++11` to build internal dependencies. When there's no pre-built binaries for your OS/architecture distribution.
|
||||
|
||||
Create the config/environments/<env>.js files (there are .example files
|
||||
to start from). You can optionally use the ./configure script for this,
|
||||
see ```./configure --help``` to see available options.
|
||||
Optional:
|
||||
|
||||
Look at lib/cartodb/server_options.js for more on config
|
||||
* [`Varnish`](http://www.varnish-cache.org)
|
||||
* [`Statsd`](https://github.com/statsd/statsd)
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
### PostGIS setup
|
||||
|
||||
Checkout your commit/branch. If you need to reinstall dependencies (you can check [NEWS](NEWS.md)) do the following:
|
||||
|
||||
```
|
||||
rm -rf node_modules; yarn
|
||||
```
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
```
|
||||
node app.js <env>
|
||||
```
|
||||
|
||||
Where <env> is the name of a configuration file under config/environments/.
|
||||
|
||||
Note that caches are kept in redis. If you're not seeing what you expect
|
||||
there may be out-of-sync records in there.
|
||||
Take a look: http://redis.io/commands
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The [docs directory](https://github.com/CartoDB/Windshaft-cartodb/tree/master/docs) contains different documentation
|
||||
resources, from higher level to more detailed ones:
|
||||
The [Maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md) defined the endpoints and their
|
||||
expected parameters and outputs.
|
||||
|
||||
|
||||
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 `yarn link`. You can read more about it at [yarn-link: Symlink a package folder](https://yarnpkg.com/en/docs/cli/link).
|
||||
|
||||
**Quick start**:
|
||||
A `template_postgis` database is expected. One can be set up with
|
||||
|
||||
```shell
|
||||
~/windshaft-directory $ yarn
|
||||
~/windshaft-directory $ yarn link
|
||||
~/windshaft-cartodb-directory $ yarn link windshaft
|
||||
$ createdb --owner postgres --template template0 template_postgis
|
||||
$ psql -d template_postgis -c 'CREATE EXTENSION postgis;'
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```shell
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
You can inject the configuration through environment variables at run time. Check the file `./config/environments/config.js` to see the ones you have available.
|
||||
|
||||
While the migration to the new environment based configuration, you can still use the old method of copying a config file. To enabled the one with environment variables you need to pass `CARTO_WINDSHAFT_ENV_BASED_CONF=true`. You can use the docker image to run it.
|
||||
|
||||
Old way:
|
||||
|
||||
```shell
|
||||
$ node app.js <env>
|
||||
```
|
||||
|
||||
Where `<env>` is the name of a configuration file under `./config/environments/`.
|
||||
|
||||
### Test
|
||||
|
||||
You can easily run the tests against the dependencies from the `dev-env`. To do so, you need to build the test docker image:
|
||||
|
||||
```shell
|
||||
$ docker-compose build
|
||||
```
|
||||
|
||||
Then you can run the tests like:
|
||||
|
||||
```shell
|
||||
$ docker-compose run windshaft-tests
|
||||
```
|
||||
|
||||
It will mount your code inside a volume. In case you want to play and run `npm test` or something else you can do:
|
||||
|
||||
```shell
|
||||
$ docker-compose run --entrypoint bash windshaft-tests
|
||||
```
|
||||
|
||||
So you will have a bash shell inside the test container, with the code from your host.
|
||||
|
||||
### Coverage
|
||||
|
||||
```shell
|
||||
$ npm run cover
|
||||
```
|
||||
|
||||
Open `./coverage/lcov-report/index.html`.
|
||||
|
||||
### Docker support
|
||||
|
||||
We provide docker images just for testing and continuous integration purposes:
|
||||
|
||||
* [`nodejs-xenial-pg1121`](https://hub.docker.com/r/carto/nodejs-xenial-pg1121/tags)
|
||||
* [`nodejs-xenial-pg101`](https://hub.docker.com/r/carto/nodejs-xenial-pg101/tags)
|
||||
|
||||
You can find instructions to install Docker, download, and update images [here](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docker/reference.md).
|
||||
|
||||
### Useful `npm` scripts
|
||||
|
||||
Run test in a docker image with a specific Node.js version:
|
||||
|
||||
```shell
|
||||
$ DOCKER_IMAGE=<docker-image-tag> NODE_VERSION=<nodejs-version> npm run test:docker
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* `<docker-image-tag>`: the tag of required docker image, e.g. `carto/nodejs-xenial-pg1121:latest`
|
||||
* `<nodejs-version>`: the Node.js version, e.g. `10.15.1`
|
||||
|
||||
In case you need to debug:
|
||||
|
||||
```shell
|
||||
$ DOCKER_IMAGE=<docker-image-tag> npm run docker:bash
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find an overview, guides, full reference, and support in [`CARTO's developer center`](https://carto.com/developers/maps-api/). The [docs directory](https://github.com/CartoDB/Windshaft-cartodb/tree/master/docs) contains different documentation resources, from a higher level to more detailed ones.
|
||||
|
||||
## Contributing
|
||||
|
||||
* The issue tracker: [`Github`](https://github.com/CartoDB/Windshaft-cartodb/issues).
|
||||
* We love Pull Requests from everyone, see [contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/#contributing).
|
||||
* You'll need to sign a Contributor License Agreement (CLA) before submitting a Pull Request. [Learn more here](https://carto.com/contributions).
|
||||
|
||||
## 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.html).
|
||||
|
||||
```shell
|
||||
$ cd /path/to/Windshaft
|
||||
$ npm install
|
||||
$ npm link
|
||||
$ cd /path/to/Windshaft-cartodb
|
||||
$ npm link windshaft
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
We follow [`SemVer`](http://semver.org/) for versioning. For available versions, see the [tags on this repository](https://github.com/CartoDB/Windshaft-cartodb/tags).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD 3-clause "New" or "Revised" License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
282
app.js
282
app.js
@@ -1,162 +1,220 @@
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
const setICUEnvVariable = require('./lib/cartodb/utils/icu_data_env_setter');
|
||||
'use strict';
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const semver = require('semver');
|
||||
|
||||
var nodejsVersion = process.versions.node;
|
||||
if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
|
||||
logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`);
|
||||
process.exit(1);
|
||||
}
|
||||
// TODO: research it it's still needed
|
||||
const setICUEnvVariable = require('./lib/utils/icu-data-env-setter');
|
||||
|
||||
// This function should be called before the require('yargs').
|
||||
setICUEnvVariable();
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
const argv = require('yargs')
|
||||
.usage('Usage: node $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'
|
||||
)
|
||||
'node $0 production -c /etc/windshaft-cartodb/config.js',
|
||||
'start server in production environment with /etc/windshaft-cartodb/config.js as config file'
|
||||
)
|
||||
.alias('h', 'help')
|
||||
.alias('c', 'config')
|
||||
.nargs('c', 1)
|
||||
.describe('c', 'Load configuration from path')
|
||||
.argv;
|
||||
|
||||
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);
|
||||
const environmentArg = argv._[0] || process.env.NODE_ENV || 'development';
|
||||
let configFileName = environmentArg;
|
||||
if (process.env.CARTO_WINDSHAFT_ENV_BASED_CONF) {
|
||||
// we override the file with the one with env vars
|
||||
configFileName = 'config';
|
||||
}
|
||||
const configurationFile = path.resolve(argv.config || `./config/environments/${configFileName}.js`);
|
||||
|
||||
global.environment = require(configurationFile);
|
||||
var ENVIRONMENT = argv._[0] || process.env.NODE_ENV || global.environment.environment;
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
process.env.NODE_ENV = argv._[0] || process.env.NODE_ENV || global.environment.environment;
|
||||
|
||||
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 || {}, {
|
||||
const agentOptions = Object.assign({
|
||||
keepAlive: false,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: Infinity,
|
||||
maxFreeSockets: 256
|
||||
});
|
||||
}, global.environment.httpAgent || {});
|
||||
|
||||
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 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 {
|
||||
log4jsConfig.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
}
|
||||
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
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/server');
|
||||
var serverOptions = require('./lib/cartodb/server_options');
|
||||
const createServer = require('./lib/server');
|
||||
const serverOptions = require('./lib/server-options');
|
||||
const { logger } = serverOptions;
|
||||
|
||||
var server = cartodbWindshaft(serverOptions);
|
||||
const availableEnvironments = {
|
||||
production: true,
|
||||
staging: true,
|
||||
development: true
|
||||
};
|
||||
|
||||
// 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
|
||||
var backlog = global.environment.maxConnections || 128;
|
||||
if (!availableEnvironments[process.env.NODE_ENV]) {
|
||||
logger.fatal(new Error(`Invalid environment ${process.env.NODE_ENV} argument, valid ones: ${Object.keys(availableEnvironments).join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
|
||||
const { engines } = require('./package.json');
|
||||
if (!semver.satisfies(process.versions.node, engines.node)) {
|
||||
logger.fatal(new Error(`Node version ${process.versions.node} is not supported, please use Node.js ${engines.node}.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var version = require("./package").version;
|
||||
const server = createServer(serverOptions);
|
||||
|
||||
listener.on('listening', function() {
|
||||
log("Using Node.js %s", process.version);
|
||||
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
|
||||
);
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
// See: https://nodejs.org/docs/latest/api/net.html#net_server_listen
|
||||
const backlog = global.environment.maxConnections || 128;
|
||||
|
||||
const listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
|
||||
const { version, name } = require('./package');
|
||||
|
||||
listener.on('listening', function () {
|
||||
const { address, port } = listener.address();
|
||||
logger.info({ 'Node.js': process.version, pid: process.pid, environment: process.env.NODE_ENV, [name]: version, address, port, config: configurationFile }, `${name} initialized successfully`);
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
function getCPUUsage (oldUsage) {
|
||||
let usage;
|
||||
|
||||
if (oldUsage && oldUsage._start) {
|
||||
usage = Object.assign({}, process.cpuUsage(oldUsage._start.cpuUsage));
|
||||
usage.time = Date.now() - oldUsage._start.time;
|
||||
} else {
|
||||
usage = Object.assign({}, process.cpuUsage());
|
||||
usage.time = process.uptime() * 1000; // s to ms
|
||||
}
|
||||
|
||||
usage.percent = (usage.system + usage.user) / (usage.time * 10);
|
||||
|
||||
Object.defineProperty(usage, '_start', {
|
||||
value: {
|
||||
cpuUsage: process.cpuUsage(),
|
||||
time: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
let previousCPUUsage = getCPUUsage();
|
||||
setInterval(function cpuUsageMetrics () {
|
||||
const CPUUsage = getCPUUsage(previousCPUUsage);
|
||||
|
||||
Object.keys(CPUUsage).forEach(property => {
|
||||
global.statsClient.gauge(`windshaft.cpu.${property}`, CPUUsage[property]);
|
||||
});
|
||||
|
||||
previousCPUUsage = CPUUsage;
|
||||
}, 5000).unref();
|
||||
|
||||
setInterval(function () {
|
||||
var memoryUsage = process.memoryUsage();
|
||||
Object.keys(memoryUsage).forEach(function(k) {
|
||||
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(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
global.logger.error('Uncaught exception: ' + err.stack);
|
||||
});
|
||||
}, 5000).unref();
|
||||
|
||||
if (global.gc) {
|
||||
var gcInterval = Number.isFinite(global.environment.gc_interval) ?
|
||||
global.environment.gc_interval :
|
||||
10000;
|
||||
var gcInterval = Number.isFinite(global.environment.gc_interval)
|
||||
? global.environment.gc_interval
|
||||
: 10000;
|
||||
|
||||
if (gcInterval > 0) {
|
||||
setInterval(function gcForcedCycle() {
|
||||
var start = Date.now();
|
||||
setInterval(function gcForcedCycle () {
|
||||
global.gc();
|
||||
global.statsClient.timing('windshaft.gc', Date.now() - start);
|
||||
}, gcInterval);
|
||||
}, gcInterval).unref();
|
||||
}
|
||||
}
|
||||
|
||||
const gcStats = require('gc-stats')();
|
||||
|
||||
gcStats.on('stats', function ({ pauseMS, gctype }) {
|
||||
global.statsClient.timing('windshaft.gc', pauseMS);
|
||||
global.statsClient.timing(`windshaft.gctype.${getGCTypeValue(gctype)}`, pauseMS);
|
||||
});
|
||||
|
||||
function getGCTypeValue (type) {
|
||||
// 1: Scavenge (minor GC)
|
||||
// 2: Mark/Sweep/Compact (major GC)
|
||||
// 4: Incremental marking
|
||||
// 8: Weak/Phantom callback processing
|
||||
// 15: All
|
||||
let value;
|
||||
|
||||
switch (type) {
|
||||
case 1:
|
||||
value = 'Scavenge';
|
||||
break;
|
||||
case 2:
|
||||
value = 'MarkSweepCompact';
|
||||
break;
|
||||
case 4:
|
||||
value = 'IncrementalMarking';
|
||||
break;
|
||||
case 8:
|
||||
value = 'ProcessWeakCallbacks';
|
||||
break;
|
||||
case 15:
|
||||
value = 'All';
|
||||
break;
|
||||
default:
|
||||
value = 'Unkown';
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const exitProcess = logger.finish((err, finalLogger, listener, signal, killTimeout) => {
|
||||
scheduleForcedExit(killTimeout, finalLogger);
|
||||
|
||||
finalLogger.info(`Process has received signal: ${signal}`);
|
||||
|
||||
let code = 0;
|
||||
|
||||
if (err) {
|
||||
code = 1;
|
||||
finalLogger.fatal(err);
|
||||
}
|
||||
|
||||
finalLogger.info(`Process is going to exit with code: ${code}`);
|
||||
listener.close(() => process.exit(code));
|
||||
});
|
||||
|
||||
function addHandlers (listener, killTimeout) {
|
||||
process.on('uncaughtException', (err) => exitProcess(err, listener, 'uncaughtException', killTimeout));
|
||||
process.on('unhandledRejection', (err) => exitProcess(err, listener, 'unhandledRejection', killTimeout));
|
||||
process.on('ENOMEM', (err) => exitProcess(err, listener, 'ENOMEM', killTimeout));
|
||||
process.on('SIGINT', () => exitProcess(null, listener, 'SIGINT', killTimeout));
|
||||
process.on('SIGTERM', () => exitProcess(null, listener, 'SIGTERM', killTimeout));
|
||||
}
|
||||
|
||||
addHandlers(listener, 45000);
|
||||
|
||||
function scheduleForcedExit (killTimeout, finalLogger) {
|
||||
// Schedule exit if there is still ongoing work to deal with
|
||||
const killTimer = setTimeout(() => {
|
||||
finalLogger.info('Process didn\'t close on time. Force exit');
|
||||
process.exit(1);
|
||||
}, killTimeout);
|
||||
|
||||
// Don't keep the process open just for this
|
||||
killTimer.unref();
|
||||
}
|
||||
|
||||
17
carto-package.json
Normal file
17
carto-package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "carto_windshaft",
|
||||
"current_version": {
|
||||
"requires": {
|
||||
"node": "^12.16.3",
|
||||
"npm": "^6.14.4",
|
||||
"mapnik": "==3.0.15.16",
|
||||
"crankshaft": "~0.8.1"
|
||||
},
|
||||
"works_with": {
|
||||
"redis": ">=4.0.0",
|
||||
"postgresql": ">=10.0.0",
|
||||
"postgis": ">=2.4.4.5",
|
||||
"carto_postgresql_ext": ">=0.35.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
411
config/environments/config.js
Normal file
411
config/environments/config.js
Normal file
@@ -0,0 +1,411 @@
|
||||
var config = {
|
||||
environment: process.env.CARTO_WINDSHAFT_NODE_ENV,
|
||||
port: 8181,
|
||||
host: null, // null on purpouse so it listens to whatever address docker assigns
|
||||
// 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,
|
||||
// Time in milliseconds to force GC cycle.
|
||||
// Disable by using <=0 value.
|
||||
gc_interval: 10000,
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
user_from_host: process.env.CARTO_WINDSHAFT_USER_FROM_HOST || '^(.*)\\.cartodb\\.com$',
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Note: each entry corresponds with an express' router.
|
||||
// You must define at least one path. However, middlewares are optional.
|
||||
routes: {
|
||||
api: [{
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1'
|
||||
],
|
||||
// Optional: attach middlewares at the begining of the router
|
||||
// to perform custom operations.
|
||||
middlewares: [
|
||||
function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: [{
|
||||
paths: [
|
||||
'/map'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}],
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: [{
|
||||
paths: [
|
||||
'/map/named'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}]
|
||||
}]
|
||||
},
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `routes` 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 `routes`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
resources_url_templates: {
|
||||
http: process.env.CARTO_WINDSHAFT_RESOURCE_URL_TEMPLATE_HTTP || 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
|
||||
https: process.env.CARTO_WINDSHAFT_RESOURCE_URL_TEMPLATE_HTTPS || 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
},
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
// See: https://nodejs.org/docs/latest/api/net.html#net_server_listen
|
||||
maxConnections: 128,
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
maxUserTemplates: 1024,
|
||||
// Seconds since "last creation" before a detached
|
||||
// or template instance map expires. Or: how long do you want
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
mapConfigTTL: 7200,
|
||||
// idle socket timeout, in milliseconds
|
||||
socket_timeout: 600000,
|
||||
enable_cors: true,
|
||||
cache_enabled: true,
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
postgres_auth_user: process.env.CARTO_WINDSHAFT_DB_USER || 'cartodb_user_<%= user_id %>',
|
||||
// Templated database password for authorized user
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
postgres_auth_pass: '<%= user_password %>',
|
||||
postgres: {
|
||||
user: 'publicuser',
|
||||
password: 'public',
|
||||
host: process.env.CARTO_WINDSHAFT_POSTGRES_HOST || 'localhost',
|
||||
port: process.env.CARTO_WINDSHAFT_POSTGRES_PORT || 5432,
|
||||
pool: {
|
||||
// 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
|
||||
}
|
||||
},
|
||||
mapnik_version: undefined,
|
||||
mapnik_tile_format: 'png8:m=h',
|
||||
statsd: {
|
||||
host: process.env.CARTO_WINDSHAFT_STATSD_HOST || 'localhost',
|
||||
port: 8125,
|
||||
prefix: process.env.CARTO_WINDSHAFT_STATSD_PREFIX || ':host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
},
|
||||
renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
// If enabled, MVTs will be generated with PostGIS directly
|
||||
// If disabled, MVTs will be generated with Mapnik MVT
|
||||
usePostGIS: true
|
||||
},
|
||||
mapnik: {
|
||||
// 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,
|
||||
|
||||
// The maximum number of waiting clients of the pool of internal mapnik backend
|
||||
// This maximum number is per mapnik renderer created in Windshaft's RendererFactory
|
||||
poolMaxWaitingClients: 64,
|
||||
|
||||
// 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.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// 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: true,
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: 'publicuser',
|
||||
password: 'public',
|
||||
host: process.env.CARTO_WINDSHAFT_POSTGRES_HOST || '127.0.0.1',
|
||||
port: process.env.CARTO_WINDSHAFT_POSTGRES_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,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {}
|
||||
},
|
||||
// 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'
|
||||
},
|
||||
// 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: process.env.CARTO_WINDSHAFT_TILE_CACHE || '/home/ubuntu/tile_assets/'
|
||||
},
|
||||
redis: {
|
||||
host: process.env.CARTO_WINDSHAFT_REDIS_HOST || '127.0.0.1',
|
||||
port: process.env.CARTO_WINDSHAFT_REDIS_PORT || 6379,
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the servelsr. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
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: process.env.CARTO_WINDSHAFT_VARNISH_PORT || 'localhost',
|
||||
port: process.env.CARTO_WINDSHAFT_VARNISH_PORT || 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: process.env.CARTO_WINDSHAFT_VARNISH_PURGE_ENABLED === 'true' || false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
fallbackTtl: 300,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
},
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
},
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
useProfiler: false,
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: process.env.CARTO_WINDSHAFT_SERVER_CDN_URL_HTTP === 'undefined' ? undefined : process.env.CARTO_WINDSHAFT_SERVER_CDN_URL_HTTP || 'api.cartocdn.com',
|
||||
https: process.env.CARTO_WINDSHAFT_SERVER_CDN_URL_HTTPS === 'undefined' ? undefined : process.env.CARTO_WINDSHAFT_SERVER_CDN_URL_HTTPS || 'cartocdn.global.ssl.fastly.net'
|
||||
}
|
||||
},
|
||||
// Settings for the health check available at /health
|
||||
health: {
|
||||
enabled: process.env.CARTO_WINDSHAFT_HEALTH_ENABLED === 'true' || false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
disabled_file: 'pids/disabled',
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: false,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: process.env.CARTO_WINDSHAFT_LAYERSTATS_ENABLED === 'true' || false,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
},
|
||||
pubSubMetrics: {
|
||||
enabled: process.env.CARTO_WINDSHAFT_METRICS_ENABLED === 'true' || false,
|
||||
project_id: process.env.CARTO_WINDSHAFT_METRICS_PROJECT_ID || 'avid-wavelet-844',
|
||||
credentials: '',
|
||||
topic: process.env.CARTO_WINDSHAFT_METRICS_PROJECT_ID || 'raw-metric-events'
|
||||
}
|
||||
};
|
||||
|
||||
// override some defaults for tests
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
config.user_from_host = '(.*)';
|
||||
config.postgres_auth_pass = 'test_windshaft_cartodb_user_<%= user_id %>_pass';
|
||||
config.millstone.cache_basedir = '/tmp/tile_assets';
|
||||
config.postgres.user = 'test_windshaft_publicuser';
|
||||
config.resources_url_templates = {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'https://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
|
||||
};
|
||||
config.cache_enabled = false;
|
||||
config.postgres_auth_user = 'test_windshaft_cartodb_user_<%= user_id %>';
|
||||
config.renderer.mapnik.postgis.twkb_encoding = false;
|
||||
config.renderer.mapnik['cache-features'] = false;
|
||||
config.renderer.http.whitelist = [ // the whitelist of urlTemplates that can be used
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png',
|
||||
// for testing purposes
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png'
|
||||
];
|
||||
config.analysis.batch.inlineExecution = true;
|
||||
config.redis.idleTimeoutMillis = 1;
|
||||
config.redis.reapIntervalMillis = 1;
|
||||
config.varnish.purge_enabled = false;
|
||||
config.health.enabled = false;
|
||||
config.enabledFeatures.layerStats = true;
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
@@ -15,34 +15,62 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
// 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
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "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)'
|
||||
// Note: each entry corresponds with an express' router.
|
||||
// You must define at least one path. However, middlewares are optional.
|
||||
,routes: {
|
||||
api: [{
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Optional: attach middlewares at the begining of the router
|
||||
// to perform custom operations.
|
||||
middlewares: [
|
||||
function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: [{
|
||||
paths: [
|
||||
'/map',
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}],
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: [{
|
||||
paths: [
|
||||
'/map/named'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
// 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
|
||||
// This URLs depend on how `routes` 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`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 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
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
// See: https://nodejs.org/docs/latest/api/net.html#net_server_listen
|
||||
,maxConnections:128
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
@@ -54,12 +82,7 @@ var config = {
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: undefined
|
||||
,cache_enabled: true
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
|
||||
@@ -67,39 +90,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
type: "postgis",
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
/* experimental
|
||||
geometry_field: "the_geom",
|
||||
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
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
pool: {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'dev.',
|
||||
prefix: 'dev.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -108,18 +117,9 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
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
|
||||
}
|
||||
//If enabled, MVTs will be generated with PostGIS directly
|
||||
//If disabled, MVTs will be generated with Mapnik MVT
|
||||
usePostGIS: true
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -128,6 +128,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// The maximum number of waiting clients of the pool of internal mapnik backend
|
||||
// This maximum number is per mapnik renderer created in Windshaft's RendererFactory
|
||||
poolMaxWaitingClients: 64,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
@@ -171,7 +175,31 @@ var config = {
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
clipByBox2d: true,
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
@@ -186,31 +214,17 @@ var config = {
|
||||
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
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -226,16 +240,7 @@ var config = {
|
||||
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
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -252,12 +257,6 @@ var config = {
|
||||
// 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: {
|
||||
@@ -311,6 +310,7 @@ var config = {
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
fallbackTtl: 300,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
@@ -326,6 +326,12 @@ var config = {
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: undefined,
|
||||
https: undefined
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
@@ -339,11 +345,38 @@ var config = {
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
onTileErrorStrategy: false,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
,pubSubMetrics: {
|
||||
enabled: false,
|
||||
project_id: '',
|
||||
credentials: '',
|
||||
topic: ''
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,34 +15,62 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
// 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
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "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)'
|
||||
// Note: each entry corresponds with an express' router.
|
||||
// You must define at least one path. However, middlewares are optional.
|
||||
,routes: {
|
||||
api: [{
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Optional: attach middlewares at the begining of the router
|
||||
// to perform custom operations.
|
||||
middlewares: [
|
||||
function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: [{
|
||||
paths: [
|
||||
'/map',
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}],
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: [{
|
||||
paths: [
|
||||
'/map/named'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
// 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
|
||||
// This URLs depend on how `routes` 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`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 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
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
// See: https://nodejs.org/docs/latest/api/net.html#net_server_listen
|
||||
,maxConnections:128
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
@@ -55,11 +83,6 @@ var config = {
|
||||
,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]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
|
||||
@@ -67,26 +90,18 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500
|
||||
port: 5432,
|
||||
pool: {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
@@ -102,18 +117,9 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
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
|
||||
}
|
||||
//If enabled, MVTs will be generated with PostGIS directly
|
||||
//If disabled, MVTs will be generated with Mapnik MVT
|
||||
usePostGIS: true
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -122,6 +128,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// The maximum number of waiting clients of the pool of internal mapnik backend
|
||||
// This maximum number is per mapnik renderer created in Windshaft's RendererFactory
|
||||
poolMaxWaitingClients: 64,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
@@ -165,7 +175,31 @@ var config = {
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
clipByBox2d: true,
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
@@ -180,33 +214,17 @@ var config = {
|
||||
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
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -222,16 +240,7 @@ var config = {
|
||||
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
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -248,12 +257,6 @@ var config = {
|
||||
// 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: {
|
||||
@@ -307,6 +310,7 @@ var config = {
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
fallbackTtl: 300,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
@@ -341,11 +345,38 @@ var config = {
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
onTileErrorStrategy: false,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: false
|
||||
layerStats: false,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
,pubSubMetrics: {
|
||||
enabled: true,
|
||||
project_id: 'avid-wavelet-844',
|
||||
credentials: '',
|
||||
topic: 'raw-metric-events'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,34 +15,61 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
// See https://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
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/maps" is the the new API,
|
||||
// "/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)'
|
||||
// Note: each entry corresponds with an express' router.
|
||||
// You must define at least one path. However, middlewares are optional.
|
||||
,routes: {
|
||||
api: [{
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Optional: attach middlewares at the begining of the router
|
||||
// to perform custom operations.
|
||||
middlewares: [
|
||||
function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: [{
|
||||
paths: [
|
||||
'/map',
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}],
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: [{
|
||||
paths: [
|
||||
'/map/named'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
// 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
|
||||
// This URLs depend on how `routes` 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`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 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',
|
||||
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
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
,maxConnections:128
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
@@ -55,11 +82,6 @@ var config = {
|
||||
,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] (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
|
||||
@@ -67,33 +89,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
port: 5432,
|
||||
pool: {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'stage.:host.',
|
||||
prefix: 'stage.:host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -102,18 +116,9 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
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
|
||||
}
|
||||
//If enabled, MVTs will be generated with PostGIS directly
|
||||
//If disabled, MVTs will be generated with Mapnik MVT
|
||||
usePostGIS: true
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -122,6 +127,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// The maximum number of waiting clients of the pool of internal mapnik backend
|
||||
// This maximum number is per mapnik renderer created in Windshaft's RendererFactory
|
||||
poolMaxWaitingClients: 64,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
@@ -165,7 +174,31 @@ var config = {
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
clipByBox2d: true,
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
@@ -180,33 +213,17 @@ var config = {
|
||||
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
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -222,16 +239,7 @@ var config = {
|
||||
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
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -248,12 +256,6 @@ var config = {
|
||||
// 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: {
|
||||
@@ -307,6 +309,7 @@ var config = {
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
fallbackTtl: 300,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
@@ -330,7 +333,7 @@ var config = {
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
@@ -341,11 +344,38 @@ var config = {
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
onTileErrorStrategy: false,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
,pubSubMetrics: {
|
||||
enabled: true,
|
||||
project_id: '',
|
||||
credentials: '',
|
||||
topic: 'raw-metric-events'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,31 +17,60 @@ var config = {
|
||||
//
|
||||
// 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
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "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)'
|
||||
// Note: each entry corresponds with an express' router.
|
||||
// You must define at least one path. However, middlewares are optional.
|
||||
,routes: {
|
||||
api: [{
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Optional: attach middlewares at the begining of the router
|
||||
// to perform custom operations.
|
||||
middlewares: [
|
||||
function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: [{
|
||||
paths: [
|
||||
'/map',
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}],
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: [{
|
||||
paths: [
|
||||
'/map/named'
|
||||
],
|
||||
middlewares: [] // Optional
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
// 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
|
||||
// This URLs depend on how `routes` 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`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 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'
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'https://{{=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
|
||||
// Specify the maximum length of the queue of pending connections for the HTTP server.
|
||||
// The actual length will be determined by the OS through sysctl settings such as tcp_max_syn_backlog and somaxconn on Linux.
|
||||
// The default value of this parameter is 511 (not 512).
|
||||
// See: https://nodejs.org/docs/latest/api/net.html#net_server_listen
|
||||
,maxConnections:128
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
@@ -54,11 +83,6 @@ var config = {
|
||||
,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]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
//,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
|
||||
@@ -66,33 +90,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "test_windshaft_publicuser",
|
||||
password: "public",
|
||||
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
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
pool: {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
,mapnik_version: ''
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'test.:host.',
|
||||
prefix: 'test.:host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -101,18 +117,9 @@ var config = {
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
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
|
||||
}
|
||||
//If enabled, MVTs will be generated with PostGIS directly
|
||||
//If disabled, MVTs will be generated with Mapnik MVT
|
||||
usePostGIS: true
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -121,6 +128,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// The maximum number of waiting clients of the pool of internal mapnik backend
|
||||
// This maximum number is per mapnik renderer created in Windshaft's RendererFactory
|
||||
poolMaxWaitingClients: 64,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
@@ -164,7 +175,31 @@ var config = {
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
clipByBox2d: true,
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
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,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: false
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
@@ -179,32 +214,17 @@ var config = {
|
||||
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
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
'cache-features': false,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -222,16 +242,7 @@ var config = {
|
||||
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
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -248,12 +259,6 @@ var config = {
|
||||
// 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: {
|
||||
@@ -307,6 +312,7 @@ var config = {
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
fallbackTtl: 300,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
@@ -322,6 +328,12 @@ var config = {
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: undefined,
|
||||
https: undefined
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
@@ -335,11 +347,38 @@ var config = {
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
onTileErrorStrategy: false,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
,pubSubMetrics: {
|
||||
enabled: false,
|
||||
project_id: '',
|
||||
credentials: '',
|
||||
topic: ''
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
81
configure
vendored
81
configure
vendored
@@ -1,81 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# This script creates config/environments/*.js files using
|
||||
# config/environments/*.js.example files as input and performing
|
||||
# settings substitutions.
|
||||
#
|
||||
# It relies on a known format of the .js.example files which haven't
|
||||
# been made easier to parse to still let humans copy them manually and
|
||||
# do further editing or leave them as such to get the same setup as before
|
||||
# the introduction of this script.
|
||||
#
|
||||
# The script is a work in progress. Available switches are printed
|
||||
# by invoking with the --help switch. More switches will be added
|
||||
# as the need/request for them arises.
|
||||
#
|
||||
# --strk(2012-07-23)
|
||||
#
|
||||
|
||||
ENVDIR=config/environments
|
||||
|
||||
PGPORT=
|
||||
MAPNIK_VERSION=
|
||||
ENVIRONMENT=development
|
||||
|
||||
STATUS="$0 $*"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTION]"
|
||||
echo
|
||||
echo "Configuration:"
|
||||
echo " --help display this help and exit"
|
||||
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
|
||||
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
|
||||
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
|
||||
}
|
||||
|
||||
while test -n "$1"; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--with-pgport=*)
|
||||
PGPORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-mapnik-version=*)
|
||||
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--environment=*)
|
||||
ENVIRONMENT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
*)
|
||||
echo "Unused option '$1'" >&2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
|
||||
|
||||
if [ -z "$PGPORT" ]; then
|
||||
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
|
||||
fi
|
||||
|
||||
echo "PGPORT: $PGPORT"
|
||||
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
|
||||
echo "ENVIRONMENT: $ENVIRONMENT"
|
||||
|
||||
o=`dirname "${ENVEX}"`/`basename "${ENVEX}" .example`
|
||||
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'/" \
|
||||
> "$o"
|
||||
|
||||
STATUSFILE=config.status--${ENVIRONMENT}
|
||||
echo "Writing ${STATUSFILE}"
|
||||
echo ${STATUS} > ${STATUSFILE} && chmod +x ${STATUSFILE}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export NPROCS=1 && export JOBS=1 && export CXX=g++-4.9 && export PGUSER=postgres
|
||||
|
||||
npm install -g yarn@0.27.5
|
||||
yarn
|
||||
|
||||
/etc/init.d/postgresql start
|
||||
|
||||
createdb template_postgis && createuser publicuser
|
||||
psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
POSTGIS_VERSION=2.4 npm test
|
||||
@@ -1,20 +0,0 @@
|
||||
# Maps API
|
||||
|
||||
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 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.
|
||||
|
||||
## Documentation
|
||||
|
||||
* [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)
|
||||
* [MapConfig File Format]([local file in the docs repo](https://github.com/CartoDB/docs/blob/master/_app/_mapsapi/06-mapconfig.md))
|
||||
114
docs/Routes.md
114
docs/Routes.md
@@ -1,114 +0,0 @@
|
||||
This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
|
||||
## Routes list
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y@:scale_factor?x.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:scale_factor(t),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik retina tiles [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik tiles [0]
|
||||
|
||||
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)/: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]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/center/:token/:z/:lat/:lng/:width/:height.:format {:user(f),:token(f),:z(f),:lat(f),:lng(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)/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
|
||||
|
||||
|
||||
## Optional deprecated routes
|
||||
|
||||
- [0] `/tiles/layergroup` is deprecated and `/api/v1/map` should be used but we keep it for now.
|
||||
- [1] `/tiles/template` is deprecated and `/api/v1/map/named` should be used but we keep it for now.
|
||||
|
||||
## How to generate the list of routes
|
||||
|
||||
Something like the following patch should do the trick
|
||||
|
||||
```javascript
|
||||
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 = 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 app;
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
1
docs/examples/01-example.md
Normal file
1
docs/examples/01-example.md
Normal file
@@ -0,0 +1 @@
|
||||
## Example 1
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Quickstart
|
||||
## Quickstart
|
||||
|
||||
## Anonymous Maps
|
||||
### Anonymous Maps
|
||||
|
||||
Here is an example of how to create an Anonymous Map with JavaScript:
|
||||
|
||||
@@ -31,7 +31,7 @@ $.ajax({
|
||||
})
|
||||
```
|
||||
|
||||
## Named Maps
|
||||
### 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:
|
||||
@@ -58,7 +58,7 @@ The following map config sets up a map of European countries that have a white f
|
||||
|
||||
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
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
@@ -66,7 +66,7 @@ curl 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}' -H 'Conte
|
||||
|
||||
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
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{username}.carto.com/api/v1/map/named/{template_id}' -H 'Content-Type: application/json'
|
||||
@@ -76,7 +76,7 @@ The response will return JSON with properties for the `layergroupid`, the timest
|
||||
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
46
docs/guides/02-general-concepts.md
Normal file
46
docs/guides/02-general-concepts.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## 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, an API Key is also required to use some of the API endpoints (e.g. to create 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.
|
||||
|
||||
|
||||
### Map Tile Rendering
|
||||
|
||||
Map tiles create the graphical representation of your map in a web browser. The performance rendering of map tiles is dependent on the type of geospatial data model (raster or vector) that you are using.
|
||||
|
||||
- **Raster**: Generates map tiles based on a grid of pixels to represent your data. Each cell is a fixed size and contains values for particular map features. On the server-side, each request queries a dataset to retrieve data for each map tile. The grid size of map tiles can often lead to graphic quality issues.
|
||||
|
||||
- **Vector**: Generates map tiles based on pre-defined coordinates to represent your data, similar to how basemap image tiles are rendered. On the client-side, map tiles represent real-world geometries of a map. Depending on the coordinates, vertices are used to connect the data and display points, lines, or polygons for the map tiles.
|
||||
|
||||
**Note:** By default, CARTO uses vector graphics for map rendering. Please [contact us](mailto:support@carto.com) if you need raster rendering enabled as part of your requirements.
|
||||
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that store geographic vector data on the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
|
||||
CARTO uses a Web Graphics Library (WebGL) to process MVT files. This is useful since WebGL's are compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles.
|
||||
|
||||
**Tip:** You can process MVT files with the [`ST_AsMVT` PostGIS function](https://postgis.net/docs/manual-dev/ST_AsMVT.html) with the [Maps API Windshaft renderer](https://github.com/CartoDB/Windshaft/blob/1000x/lib/windshaft/renderers/pg_mvt/renderer.js).
|
||||
@@ -1,18 +1,18 @@
|
||||
# Anonymous Maps
|
||||
## 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).
|
||||
Alternatively, you can get the data for the map (geometry and attributes for each layer) using vector tiles (in which case CartoCSS is not required).
|
||||
|
||||
|
||||
## Instantiate
|
||||
### Instantiate
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -29,9 +29,9 @@ POST /api/v1/map
|
||||
}
|
||||
```
|
||||
|
||||
See [MapConfig File Formats](http://docs.carto.com/carto-engine/maps-api/mapconfig/) for details.
|
||||
See [MapConfig File Formats]({{site.mapsapi_docs}}/guides/MapConfig-file-format/) for details.
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
@@ -49,15 +49,15 @@ Now, for convenience, the layergroup includes the final URLs in two formats:
|
||||
1. Leaflet's urlTemplate alike: useful when working with raster tiles or with libraries with an API similar to Leaflet's one.
|
||||
1. [TileJSON spec](https://github.com/mapbox/tilejson-spec): useful when working with Mapbox GL or any other library that supports TileJSON.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -97,7 +97,7 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
}
|
||||
```
|
||||
|
||||
## Map Tile Rendering
|
||||
### Map Tile Rendering
|
||||
|
||||
Map tiles are used to create the graphic representation of your map in a web browser. Tiles can be requested either as pre-rendered *raster* tiles (images) or as *vector* map data to be rendered by the client (browser).
|
||||
|
||||
@@ -105,11 +105,11 @@ Map tiles are used to create the graphic representation of your map in a web bro
|
||||
|
||||
- **Vector**: Tiles can also be requested as MVT (Mapbox Vector Tiles). In this case, only the geospatial vector data, without any styling, is returned. These tiles should be processed in the client-side to render the map. In this case layers do not need to define CartoCSS, as any rendering and styling will be performed on the client side. The vector data of a tile represents real-world geometries by defining the vertices of points, lines or polygons in a tile-specific coordinate system.
|
||||
|
||||
## Retrieve resources from the layergroup
|
||||
### 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.
|
||||
|
||||
### Raster tiles
|
||||
#### Raster tiles
|
||||
|
||||
These raster tiles are PNG images that represent only the Mapnik layers of a map. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
|
||||
@@ -117,7 +117,7 @@ These raster tiles are PNG images that represent only the Mapnik layers of a map
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
#### Mapbox Vector Tiles (MVT)
|
||||
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that transfer geographic vector data to the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
|
||||
@@ -125,7 +125,7 @@ CARTO uses Web Graphics Library (WebGL) to process MVT files on the browser. Thi
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
#### MVT and Windshaft
|
||||
##### MVT and Windshaft
|
||||
|
||||
CARTO uses Windshaft as the map tiler library to render multilayer maps with the Maps API. You can use Windshaft to request MVT using the same layer type that is used for requesting raster tiles (Mapnik layer). Simply change the file format `.mvt` in the URL.
|
||||
|
||||
@@ -149,7 +149,7 @@ The following example instantiates an anonymous map with layer options:
|
||||
|
||||
**Note**: If no layer type is specified, Mapnik tiles are used by default. To access MVT tiles, specify `https://{username}.cartodb.com/api/v1/map/HASH/{z}/{x}/{y}.mvt` as the `maps_api_template` variable.
|
||||
|
||||
**Tip:** If you are using [Named Maps](https://carto.com/docs/carto-engine/maps-api/named-maps/) to instantiate a layer, indicate the MVT file format and layer in the response:
|
||||
**Tip:** If you are using [Named Maps]({{site.mapasapi_docs}}/guides/named-maps/) to instantiate a layer, indicate the MVT file format and layer in the response:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/:layer/{z}/{x}/{y}.mvt
|
||||
@@ -161,7 +161,7 @@ For all layers in a Named Map, you must indicate Mapnik as the layer filter:
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/mapnik/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Layergroup Filter for MVT Tiles
|
||||
##### Layergroup Filter for MVT Tiles
|
||||
|
||||
To filter layers using Windshaft, use the following request where layers are numbered:
|
||||
|
||||
@@ -181,7 +181,7 @@ To filter a specific layer:
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/2/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Example 1: MVT Tiles with Windshaft, CARTO.js, and MapboxGL
|
||||
##### Example 1: MVT Tiles with Windshaft, CARTO.js, and MapboxGL
|
||||
|
||||
1) Import the required libraries:
|
||||
|
||||
@@ -223,7 +223,7 @@ cartocss: "...",
|
||||
|
||||
5) Request Tiles (from CARTO) and Set to Map Object (Mapbox):
|
||||
|
||||
**Note:** By default, [CARTO core functions](https://carto.com/docs/carto-engine/carto-js/core-api/) retrieve URLs for fully rendered tiles. You must replace the default format (.png) with the MVT format (.mvt).
|
||||
**Note:** By default, [CARTO core functions]({{site.cartojs_docs}}/v3/guides/core-API-functionality/) retrieve URLs for fully rendered tiles. You must replace the default format (.png) with the MVT format (.mvt).
|
||||
|
||||
|
||||
```bash
|
||||
@@ -237,7 +237,7 @@ map.setStyle(simpleStyle(tiles));
|
||||
});
|
||||
```
|
||||
|
||||
#### Example 2: MVT Libraries with Windshaft and MapboxGL
|
||||
##### Example 2: MVT Libraries with Windshaft and MapboxGL
|
||||
|
||||
When you are not including CARTO.js to implement MVT tiles, you must use the `map.setStyle` parameter to specify vector map rendering.
|
||||
|
||||
@@ -291,7 +291,7 @@ map.setStyle({
|
||||
- [MapboxGL Style Specifications](https://www.mapbox.com/mapbox-gl-js/style-spec/)
|
||||
- [Example of MapboxGL Implementation](https://www.mapbox.com/mapbox-gl-js/examples/)
|
||||
|
||||
### Individual layers
|
||||
#### 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.
|
||||
|
||||
@@ -309,7 +309,7 @@ If the MapConfig had a Torque layer at index 1 it could be possible to request i
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
### Attributes defined in `attributes` section
|
||||
#### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
@@ -321,7 +321,7 @@ Which returns JSON with the attributes defined, such as:
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
### Blending and layer selection
|
||||
#### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
@@ -352,17 +352,17 @@ 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 that you will always get consistent behavior.
|
||||
|
||||
## Create JSONP
|
||||
### Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
@@ -370,15 +370,15 @@ config | Encoded JSON with the params for creating Named Maps (the variables def
|
||||
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
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### 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
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
@@ -391,6 +391,6 @@ callback({
|
||||
})
|
||||
```
|
||||
|
||||
## Remove
|
||||
### 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Named Maps
|
||||
## 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).
|
||||
|
||||
@@ -6,7 +6,7 @@ The Named Map workflow consists of uploading a MapConfig file to CARTO servers,
|
||||
|
||||
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.
|
||||
**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]({{site.cartojs_docs}}/v3/guides/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:
|
||||
|
||||
@@ -16,26 +16,26 @@ The main differences, compared to Anonymous Maps, is that Named Maps include:
|
||||
- **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/).
|
||||
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]({{site.mapsapi_docs}}/guides/MapConfig-file-format/).
|
||||
|
||||
**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).
|
||||
**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]({{site.mapsapi_docs}}/guides/MapConfig-file-format/#named-map-layer-options).
|
||||
|
||||
## Create
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### 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
|
||||
MapConfig | a [Named Map MapConfig]({{site.mapsapi_docs}}/guides/MapConfig-file-format/#named-map-layer-options) is required to create a Named Map
|
||||
|
||||
#### template.json
|
||||
##### 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.
|
||||
|
||||
@@ -93,7 +93,7 @@ The `name` argument defines how to name this "template_name".json. Note that the
|
||||
}
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
##### Arguments
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
@@ -104,7 +104,7 @@ 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.
|
||||
layergroup | the layergroup configurations, as specified in the template. See [MapConfig File Format]({{site.mapsapi_docs}}/guides/MapConfig-file-format/) 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
|
||||
@@ -122,13 +122,13 @@ view (optional) | extra keys to specify the view area for the map. It can be use
|
||||
|_ |_ north | UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
|
||||
### Placeholder Format
|
||||
#### 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 [Layergroup configurations](http://docs.carto.com/carto-engine/maps-api/mapconfig/#layergroup-configurations).
|
||||
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 [Layergroup configurations]({{site.mapsapi_docs}}/guides/MapConfig-file-format/#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
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
@@ -136,7 +136,7 @@ Valid placeholder names start with a letter and can only contain letters, number
|
||||
|
||||
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
|
||||
#### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
@@ -151,7 +151,7 @@ Placeholder default values will be used whenever new values are not provided as
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined loosely.
|
||||
|
||||
#### Call
|
||||
##### 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.
|
||||
|
||||
@@ -162,7 +162,7 @@ curl -X POST \
|
||||
'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### 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.
|
||||
|
||||
@@ -172,17 +172,17 @@ The response back from the API provides the name of your MapConfig as a template
|
||||
}
|
||||
```
|
||||
|
||||
## Instantiate
|
||||
### 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
|
||||
##### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Param
|
||||
##### Param
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
@@ -200,14 +200,14 @@ The fields you pass as `params.json` depend on the variables allowed by the Name
|
||||
|
||||
**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
|
||||
##### 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
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
@@ -216,7 +216,7 @@ curl -X POST \
|
||||
'https://{username}.carto.com/api/v1/map/named/{template_name}?auth_token={auth_token}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -225,7 +225,7 @@ curl -X POST \
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
##### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -233,33 +233,33 @@ curl -X POST \
|
||||
}
|
||||
```
|
||||
|
||||
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/)).
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see [Anonymous Maps]({{site.mapsapi_docs}}/guides/anonymous-maps/)).
|
||||
|
||||
## Update
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
PUT /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
### Other Information
|
||||
#### Other Information
|
||||
|
||||
Updating a Named Map removes all the Named Map instances, so they need to be initialized again.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
@@ -268,7 +268,7 @@ curl -X PUT \
|
||||
'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -286,31 +286,31 @@ If a template with the same name does NOT exist, a 400 HTTP response is generate
|
||||
}
|
||||
```
|
||||
|
||||
## Delete
|
||||
### Delete
|
||||
|
||||
Deletes the specified template map from the server, and disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X DELETE 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -320,31 +320,31 @@ curl -X DELETE 'https://{username}.carto.com/api/v1/map/named/{template_name}?ap
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
## Listing Available Templates
|
||||
### Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -352,7 +352,7 @@ curl -X GET 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
##### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -360,31 +360,31 @@ curl -X GET 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
}
|
||||
```
|
||||
|
||||
## Get Template Definition
|
||||
### Get Template Definition
|
||||
|
||||
This gets the definition of a requested template.
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -392,7 +392,7 @@ curl -X GET 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_k
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
##### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -400,17 +400,17 @@ curl -X GET 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_k
|
||||
}
|
||||
```
|
||||
|
||||
## JSONP for Named Maps
|
||||
### 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
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/{template_name}/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
@@ -419,13 +419,13 @@ 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
|
||||
##### 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
|
||||
##### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
@@ -454,9 +454,9 @@ callback({
|
||||
})
|
||||
```
|
||||
|
||||
## CARTO.js for Named Maps
|
||||
### 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.
|
||||
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]({{site.cartojs_docs}}/v3/guides/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -486,15 +486,15 @@ You can use a Named Map that you created (which is defined by its `name`), to cr
|
||||
|
||||
**Note:** Instantiating a Named Map over a `createLayer` does not require an API Key and by default, does not include auth tokens. _If_ you defined auth tokens for the Named Map configuration, then you will have to include them.
|
||||
|
||||
[CARTO.js](http://docs.carto.com/carto-engine/carto-js/) has methods for accessing your Named Maps.
|
||||
[CARTO.js]({{site.cartojs_docs}}/v3/) 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
|
||||
1. [layer.setParams()]({{site.cartojs_docs}}/v3/reference/#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
|
||||
2. [layer.setAuthToken()](h{{site.cartojs_docs}}/v3/reference/#layersetauthtokenauth_token) allows you to set the auth tokens to create the layer
|
||||
|
||||
### Torque Layer in a Named Map
|
||||
#### 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:
|
||||
|
||||
@@ -525,7 +525,7 @@ If you are creating a Torque layer in a Named Map without using the Torque.js li
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples of Named Maps created with CARTO.js
|
||||
##### Examples of Named Maps created with CARTO.js
|
||||
|
||||
- [Named Map selectors with interaction](http://bl.ocks.org/andy-esch/515a8af1f99d5e690484)
|
||||
|
||||
@@ -533,11 +533,11 @@ If you are creating a Torque layer in a Named Map without using the Torque.js li
|
||||
|
||||
- [Toggling sublayers in a Named Map](http://bl.ocks.org/andy-esch/c1a0f4913610eec53cd3)
|
||||
|
||||
## Fetching XYZ Tiles for Named Maps
|
||||
### 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
|
||||
#### 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.
|
||||
|
||||
@@ -551,17 +551,17 @@ For example, a complete URL might appear as:
|
||||
|
||||
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.
|
||||
- [`template_id`]({{site.mapsapi_docs}}/guides/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"
|
||||
- To show a [list of layers]({{site.mapsapi_docs}}/guides/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
|
||||
#### 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.
|
||||
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]({{site.mapsapi_docs}}/guides/named-maps/#response-1) to initialize the map.
|
||||
|
||||
Instantiate the map by using your `layergroupid` in the token placeholder:
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Static Maps API
|
||||
## 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.
|
||||
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
|
||||
### 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.
|
||||
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
|
||||
#### Zoom + center
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}
|
||||
{% raw %}GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}{{?}extra_options}{% endraw %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
token | the layergroupid token from the map instantiation
|
||||
token | the `layergroupid` token from the map instantiation
|
||||
z | the zoom level of the map
|
||||
lat | the latitude for the center of the map
|
||||
|
||||
@@ -26,19 +26,19 @@ format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
### Bounding Box
|
||||
#### Bounding Box
|
||||
|
||||
#### Definition
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/{token}/{bbox}/{width}/{height}.{format}`
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
token | the layergroupid token from the map instantiation
|
||||
token | the `layergroupid` token from the map instantiation
|
||||
|
||||
bbox | the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
--- | ---
|
||||
@@ -57,16 +57,19 @@ Note: you can see this endpoint as
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format}`
|
||||
```
|
||||
#### Extra options
|
||||
* Layer: List of layers to be shown in the image (by default `all`), for example `?layer=0,1`.
|
||||
|
||||
### Named Map
|
||||
|
||||
#### Definition
|
||||
#### Named Map
|
||||
|
||||
##### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/named/{name}/{width}/{height}.{format}
|
||||
```
|
||||
|
||||
#### Params
|
||||
##### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
@@ -78,9 +81,9 @@ 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`.
|
||||
A Named Maps static image will get its constraints from the [`view` argument of the Create Named Map function]({{site.mapasapi_docs}}/guides/named-maps/). 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
|
||||
##### 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.
|
||||
|
||||
@@ -124,7 +127,7 @@ By manipulating the `"urlTemplate"` custom basemaps can be used in generating st
|
||||
|
||||
**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.
|
||||
As described in the [MapConfig File Format]({{site.mapsapi_docs}}/guides/MapConfig-file-format/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -140,11 +143,11 @@ As described in the [MapConfig File Format](http://docs.carto.com/carto-engine/m
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
### Caching
|
||||
#### 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.
|
||||
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
|
||||
#### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the limit for pixel range is 8192 x 8192.
|
||||
* Image resolution is set to 72 DPI
|
||||
@@ -156,21 +159,21 @@ It is important to note that generated images are cached from the live data refe
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>
|
||||
{% endhighlight %}
|
||||
|
||||
## Examples
|
||||
### Examples
|
||||
|
||||
After instantiating a map from a CARTO account:
|
||||
|
||||
#### Call
|
||||
##### Call
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/{layergroupid}/{z}/{x}/{y}/{width}/{height}.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
##### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
### MapConfig
|
||||
#### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Tile Aggregation
|
||||
## Tile Aggregation
|
||||
|
||||
To be able to represent a large amount of data (say, hundred of thousands to millions of points) in a tile. This can be useful both for raster tiles (where the aggregation reduces the number of features to be rendered) and vector tiles (the tile contais less features).
|
||||
|
||||
@@ -6,23 +6,23 @@ Aggregation is available only for point geometries. During aggregation the point
|
||||
- The position of the aggregated point is controlled by the `placement` parameter.
|
||||
- The aggregated rows always contain at least a column, named `_cdb_feature_count`, which contains the number of the original points that the aggregated point represents.
|
||||
|
||||
### Special default aggregation
|
||||
#### Special default aggregation
|
||||
|
||||
When no placement or columns are specified a special default aggregation is performed.
|
||||
|
||||
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_features_count` with the number of features in the group.
|
||||
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_feature_count` with the number of features in the group.
|
||||
|
||||
Regarding the randomness of the sample: currently we use the row with the minimum `cartodb_id` value in each group.
|
||||
|
||||
The rationale behind having this special aggregation with all the original columns is to provide a mostly transparent way to handle large datasets without having to provide special map configurations for those cases (i.e. preserving the logic used to produce the maps with smaller datasets). [Overviews have been used so far with this intent](https://carto.com/docs/tips-and-tricks/back-end-data-performance/), but they are inflexible.
|
||||
|
||||
### User defined aggregations
|
||||
#### User defined aggregations
|
||||
|
||||
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_features_count`, which is always present.
|
||||
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_feature_count`, which is always present.
|
||||
|
||||
We might decide in the future to allow sampling column values for any of the different placement modes.
|
||||
|
||||
### Behaviour for raster and vector tiles
|
||||
#### Behaviour for raster and vector tiles
|
||||
|
||||
The vector tiles from a vector-only map will be aggregated by default.
|
||||
However, Raster tiles (or vector tiles from a map which defines CartoCSS styles) will be aggregated only upon request.
|
||||
@@ -79,7 +79,7 @@ layer will be aggregated when tiles are requested, both for vector (mvt) and ras
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregation parameters
|
||||
### Aggregation parameters
|
||||
|
||||
The aggregation parameters for a layer are defined inside an `aggregation` option of the layer:
|
||||
|
||||
@@ -96,24 +96,24 @@ The aggregation parameters for a layer are defined inside an `aggregation` optio
|
||||
}
|
||||
```
|
||||
|
||||
### `placement`
|
||||
#### `placement`
|
||||
|
||||
Determines the kind of aggregated geometry generated:
|
||||
|
||||
#### `point-sample`
|
||||
##### `point-sample`
|
||||
|
||||
This is the default placement. It will place the aggregated point at a random sample of the grouped points,
|
||||
like the default aggregation does. No other attribute is sampled, though, the point will contain the aggregated attributes determined by the `columns` parameter.
|
||||
|
||||
#### `point-grid`
|
||||
##### `point-grid`
|
||||
|
||||
Generates points at the center of the aggregation grid cells (squares).
|
||||
|
||||
#### `centroid`
|
||||
##### `centroid`
|
||||
|
||||
Generates points with the averaged coordinated of the grouped points (i.e. the points inside each grid cell).
|
||||
|
||||
### `columns`
|
||||
#### `columns`
|
||||
|
||||
The aggregated attributes defined by `columns` are computed by a applying an _aggregate function_ to all the points in each group.
|
||||
Valid aggregate functions are `sum`, `avg` (average), `min` (minimum), `max` (maximum) and `mode` (the most frequent value in the group).
|
||||
@@ -134,9 +134,9 @@ of the original dataset applying three different aggregate functions.
|
||||
|
||||
> Note that you can use the original column names as names of the result, but all the result column names must be unique. In particular, the names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used for aggregated columns, as they correspond to columns always present in the result.
|
||||
|
||||
### `resolution`
|
||||
#### `resolution`
|
||||
|
||||
Defines the cell-size of the spatial aggregation grid. This is equivalent to the [CartoCSS `-torque-resolution`](https://carto.com/docs/carto-engine/cartocss/properties-for-torque/#-torque-resolution-float) property of Torque maps.
|
||||
Defines the cell-size of the spatial aggregation grid. This is equivalent to the [CartoCSS `-torque-resolution`]({{site.styling_cartocss}}/#-torque-resolution-float) property of Torque maps.
|
||||
|
||||
The aggregation cells are `resolution`×`resolution` pixels in size, where pixels here are defined to be 1/256 of the (linear) size of a tile.
|
||||
The default value is 1, so that aggregation coincides with raster pixels. A value of 2 would make each cell to be 4 (2×2) pixels, and a value of
|
||||
@@ -145,11 +145,11 @@ The default value is 1, so that aggregation coincides with raster pixels. A valu
|
||||
> Note that is independent of the number of pixels for raster tile or the coordinate resolution (mvt_extent) of vector tiles.
|
||||
|
||||
|
||||
### `threshold`
|
||||
#### `threshold`
|
||||
|
||||
This is the minimum number of (estimated) rows in the dataset (query results) for aggregation to be applied. If the number of rows estimate is less than the threshold aggregation will be disabled for the layer; the instantiation response will reflect that and tiles will be generated without aggregation.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -185,3 +185,80 @@ This is the minimum number of (estimated) rows in the dataset (query results) fo
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `filters`
|
||||
|
||||
Aggregated data can be filtered by imposing filtering conditions on the aggregated columns.
|
||||
|
||||
Each condition is represented by one or more parameters:
|
||||
|
||||
* `{ "equal": V }` selects an specific value of the aggregated column.
|
||||
* `{ "not_equal": V }` selects values different from the one specified.
|
||||
* `{ "in": [v1, v2, v3] }` selects any value from a list.
|
||||
* `{ "not_in": [v1, v2, v3] }` selects any value not in a list.
|
||||
* `{ "less_than": v }` selects values strictly less than the one given.
|
||||
* `{ "less_than_or_equal_to": v }` selects values less than or equal to the one given.
|
||||
* `{ "greater_than": v }` selects values strictly greater than the one given.
|
||||
* `{ "greater_than_or_equal_to": v }` selects values greater than or equal to the one given.
|
||||
|
||||
One of the *less* conditions can be combined with one of the *greater* conditions to select a range of values, for example:
|
||||
* `{ "greater_than": v1, "less_than": v2 }`
|
||||
* `{ "greater_than_or_equal_to": v1, "less_than": v2 }`
|
||||
* `{ "greater_than": v1, "less_than_or_equal_to": v2 }`
|
||||
* `{ "greater_than_or_equal_to": v1, "less_than_or_equal_to": v2 }`
|
||||
|
||||
For a given column, multiple conditions can be passed in an array; the conditions will logically ORed (any of the conditions have to be verifid for the value to be selected):
|
||||
|
||||
* `"myvalue": [ { "equal": 10 }, { "less_than": 0 }]` will select values of the column `myvalue` which are equal to 10 **or** less than 0.
|
||||
|
||||
In addition, the filters applied to different columns are logically combined with AND (all the conditions have to be satisfied for an element to be selected); for example with the following `filters` parameter we'll select aggregated records which have a `total_value` > 100 **and** a category equal to "a".
|
||||
|
||||
```json
|
||||
{
|
||||
"total_value": { "greater_than": 100 },
|
||||
"category": { "equal": "a" }
|
||||
}
|
||||
```
|
||||
|
||||
Note that the filtered columns have to be defined with the `columns` parameter, except for `_cdb_feature_count`, which is always implicitly defined and can be filtered too.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.7.0",
|
||||
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
|
||||
"srid": 3857,
|
||||
"maxzoom": 18,
|
||||
"minzoom": 3,
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#table { marker-width: [total]; marker-fill: ramp(value, (red, green, blue), jenks); }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"aggregation": {
|
||||
"placement": "centroid",
|
||||
"columns": {
|
||||
"total_value": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
},
|
||||
"category": {
|
||||
"aggregate_function": "mode",
|
||||
"aggregated_column": "category"
|
||||
}
|
||||
},
|
||||
"filters" : {
|
||||
"total_value": { "greater_than": 100 },
|
||||
"category": { "equal": "a" }
|
||||
},
|
||||
"resolution": 2,
|
||||
"threshold": 500000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
270
docs/guides/07-MapConfig-file-format.md
Normal file
270
docs/guides/07-MapConfig-file-format.md
Normal file
@@ -0,0 +1,270 @@
|
||||
{% comment %}
|
||||
The original resource for this was:
|
||||
https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.4.0.md. However this is internal documenation only. This file (07-mapconfig.md) contains select content from the Windshaft internal doc. *I instructed @rochoa to add new Doc issues if/when they make a change to this content - so that the public docs can also be updated.
|
||||
{% endcomment %}
|
||||
|
||||
## MapConfig File Format
|
||||
|
||||
CARTO uses Windshaft as the map tiler library to render multilayer maps with the [Maps API]({{ site.mapsapi_docs }}/). The MapConfig file is where these Windshaft layers are stored and applied. You can configure tiles and use the MapConfig document to request different resources for your map.
|
||||
|
||||
This section describes the MapConfig specifications, and required formats, when using the Maps API.
|
||||
|
||||
### Layergroup Configurations
|
||||
|
||||
The following MapConfig Layergroup configurations are applied using the [RFC 4627](http://www.ietf.org/rfc/rfc4627.txt) JSON format.
|
||||
|
||||
Layergroup Configuration | Description | Optional or Required?
|
||||
--- | ---
|
||||
`version` | Spec version to use for validation.<br /><br />**Note:** The default value is `"1.0.0"`. | Optional
|
||||
`extent` | The default map extent for the map projection.<br /><br />**Note:** Currently, only webmercator is supported. | Optional
|
||||
`srid` | The spatial reference identifier for the map. The default is `3857`. | Optional
|
||||
`maxzoom` | The maximum zoom level for your map. A request beyond the defined maxzoom returns a 404 error.<br /><br />**Note:** The default value is undefined (infinite). | Optional
|
||||
`minzoom` | The minimum zoom level for your map. A request beyond the defined minzoom returns a 404 error.<br /><br />**Note:** The default value is `0`. | Optional
|
||||
`layers` | Defines the layer type, and the layers, in rendering order.<br /><br />**Note:** The following layers options are available: |
|
||||
--- | ---
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> type | A string value that defines the layer type. You can define up to four values:<br /><br />`mapnik`, rasterized tiles<br /><br />`cartodb`, an alias for mapnik (for backward compatibility)<br /><br />`torque`, render vector tiles in torque format<br /><br />`http`, load tiles over HTTP<br /><br />`plain`, color or background image url<br /><br />`named`, use a Named Map as a layer | Required
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> options | An object value that sets different options for each layer type.<br /><br />**Note:** Options that are not defined in different layers will be discarded. | Required
|
||||
|
||||
#### Example of MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"version": "1.7.0",
|
||||
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
|
||||
"srid": 3857,
|
||||
"maxzoom": 18,
|
||||
"minzoom": 3,
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#table { marker-placement: point; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
---
|
||||
|
||||
### Mapnik Layer Options
|
||||
|
||||
If you are using Mapnik as a layer resource, the following configurations are required in your MapConfig file.
|
||||
|
||||
Mapnik Layer Option | Description | Optional or Required?
|
||||
--- | ---
|
||||
`sql` | A string value, the SQL request to the user database that will fetch the rendered data.<br /><br />**Tip:** The SQL request should include the following Mapnik layer configurations: `geom_column`, `interactivity`, and `attributes`, as described in this section.<br /><br />**Note:** The SQL request may contain substitutions tokens, such as `!bbox!`, `!pixel_width!`, and `!pixel_height!`. It is suggested to define the layergroup `minzoom` and `extent` variables to prevent errors. | Required
|
||||
`cartocss` | A string value, specifying the CartoCSS style to render the tiles. If this is not present, only vector tiles can be requested for this layer. For a map to be valid either all the layers or none of them must have CartoCSS style.<br /><br />**Note:** The CartoCSS specification is dependent on the layer type. For details, see [mapnik-reference.json](https://github.com/mapnik/mapnik-reference). | Optional
|
||||
`cartocss_version` | A string value, specifying the CartoCSS style version of the CartoCSS attribute.<br /><br />**Note:** The CartoCSS version is specific to the layer type. | Optional
|
||||
`geom_column` | The name of the column containing the geometry. The default is `the_geom_webmercator`.<br /><br />*You must specify this value as part of the Mapnik layer `SQL`configuration. | *Optional
|
||||
`geom_type` | Defines the type of column as either `geometry` (the default) or `raster`.<br /><br />**Note:** `geom_type` is not compatible with the Mapnik layer `interactivity` option. | Optional
|
||||
`raster_band` | Defines the raster band (this option is only applicable when the `geom_type=raster`. The default value is `0`.<br /><br />**Note:** If the default, or no value is specified, raster bands are interpreted as either: grayscale (for single bands), RGB (for 3 bands), or RGBA (for 4 bands). | Optional
|
||||
`srid` | The spatial reference identifier for the geometry column. The default is `3857`. | Optional
|
||||
`affected_tables` | A string of values containing the tables that the Mapnik layer `SQL` configuration is using. This value is used if there is a problem guessing what the affected tables are from the SQL configuration (i.e. when using PL/SQL functions). | Optional
|
||||
`interactivity` | A string of values that contains the fields rendered inside grid.json. All the parameters should be exposed as a result of executing the Mapnik layer `SQL` query.<br /><br />**Note:** `interactivity` is not compatible with the Mapnik layer `geom_type` option. For example, you cannot create a layergroup instance with a raster layer by defining the `geom_type=raster`.<br /><br />*You must specify this value as part of the Mapnik layer `SQL` configuration. | *Optional
|
||||
`attributes`<a name="attributes"></a> | The id and column values returned by the Mapnik attributes service. (This option is disabled if no configuration is defined).<br /><br />*You must specify this value as part of the Mapnik layer `SQL`configuration.| *Optional
|
||||
--- | ---
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> id | The key value used to fetch columns. | Required
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> columns | A string of values (columns) returned by the Mapnik attribute service. | Required
|
||||
|
||||
#### Example of Mapnik MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#layer { marker-placement: point; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"geom_column": "the_geom_webmercator",
|
||||
"geom_type": "geometry",
|
||||
"interactivity": [ "column1", "column2", "..."],
|
||||
"attributes": {
|
||||
"id": "cartodb_id",
|
||||
"columns": ["column1", "column2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### Torque Layer Options
|
||||
|
||||
If you are using Torque as a layer resource, the following configurations are required in your MapConfig file. For more details about Torque layers in general, see the [Torque API]({{ site.torque_docs}}/reference/) documentation.
|
||||
|
||||
Torque Layer Option | Description | Optional or Required?
|
||||
--- | ---
|
||||
`sql` | A string value, the SQL request to the user database that will fetch the rendered data.<br /><br />**Tip:** The SQL request should include the following Torque layer configurations: `geom_column`, `interactivity`, and `attributes`, as described in this section. | Required
|
||||
`cartocss` | A string value, specifying the CartoCSS style to render the tiles.<br /><br />**Note:** The CartoCSS specification is dependent on the layer type. For details, see [Torque cartocss-reference.js](https://github.com/CartoDB/torque/blob/master/lib/torque/cartocss_reference.js).| Required
|
||||
`cartocss_version` | A string value, specifying the CartoCSS style version of the CartoCSS attribute.<br /><br />**Note:** The CartoCSS version is specific to the layer type. | Required
|
||||
`step` | The number of [animation steps]({{site.styling_cartocss}}/-#torque-frame-count-number) to render when requesting a torque.png tile. The default value is `0`. | Optional
|
||||
`geom_column` | The name of the column containing the geometry. The default is `the_geom_webmercator`.<br /><br />*You must specify this value as part of the Torque layer `SQL`configuration. | *Optional
|
||||
`srid` | The spatial reference identifier for the geometry column. The default is `3857`. | Optional
|
||||
`affected_tables` | A string of values containing the tables that the Mapnik layer `SQL` configuration is using. This value is used if there is a problem guessing what the affected tables are from the SQL configuration (i.e. when using PL/SQL functions). | Optional
|
||||
`attributes` | The id and column values returned by the Torque attributes service. (This option is disabled if no configuration is defined).<br /><br />*You must specify this value as part of the Torque layer `SQL`configuration.| *Optional
|
||||
--- | ---
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> id | The key value used to fetch columns. | Required
|
||||
<i class="Icon Icon--s5 Icon--cGrey Icon--mAlign Icon--indent"></i> columns | A string of values (columns) returned by the Torque attribute service. | Required
|
||||
|
||||
#### Example of Torque MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"type": "torque",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#layer { ... }",
|
||||
"cartocss_version": "1.0.0",
|
||||
"geom_column": "the_geom_webmercator"
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### HTTP Layer Options
|
||||
|
||||
If you are using an HTTP destination as the resource for a map layer, the following configurations are required in your MapConfig file.
|
||||
|
||||
HTTP Layer Option | Description | Optional or Required?
|
||||
--- | ---
|
||||
`urlTemplate` | A string value, end URL, from where the tile data is retrieved. _URLs must be included in the configuration whitelist to be valid._ <br /><br />**Note:** The {String} value includes:<br /><br />`{z}` as the zoom level<br /><br />`{x} and {y}` as the tile coordinates<br /><br />Optionally, the subdomain `{s}` may be included as part of the `urlTemplate` configuration. Otherwise, you can define the `subdomains` separately, as shown below. | Required
|
||||
`subdomains` | A string of values used to retrieve tiles from different subdomains. The default value is [`a`, `b`, `c`] when `{s}` is defined in the `urlTemplate` configuration. Otherwise, the default value is `[ ]`.<br /><br />**Note:** The subdomains value will consistently replace the `{s}` value defined in the `urlTemplate`.| Optional
|
||||
`tms` | A boolean value that specifies whether the tile is using Tile Map Service format. The default value is `false`.<br /><br />**Note:** If the value is `true`, the TMS inverses the Y axis numbering for tiles. | Optional
|
||||
|
||||
#### Example of HTTP MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.example.com/{z}/{x}/{y}.png",
|
||||
"subdomains": ["a", "b", "c"],
|
||||
"tms": false
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### Plain Layer Options
|
||||
|
||||
If you are using plain layer options as your map resource, the following configurations are required in your MapConfig file.
|
||||
|
||||
_**Note:** At least one of the plain layer options (either `color` or `imageUrl`) must be defined. If both options are defined, only `color` is used as part of the plain layer configuration._
|
||||
|
||||
Plain Layer Option | Description | Optional or Required?
|
||||
--- | ---
|
||||
`color` | A string value of numbers that defines the valid colors to include. The default value is `null`. Valid colors include:<br /><br />- A string value that includes CSS colors (i.e. `blue`) or a hex color string (i.e. `#0000ff`)<br /><br />- An integer array of r,g,b values (i.e. `[255,0,0]`)<br /><br />- An integer array of r,g,b,a values (i.e. `[255,0,0,128]`)<br /><br />* If **only** the `color` value is used for a plain layer, this value is Required.<br /><br />* If **both** `color` and `imageUrl` are defined, only the `color` value is used for the plain layer configuration.| *Both
|
||||
`imageUrl` | A string value, end URL, from where the image is retrieved. The default value is `null`.<br /><br />* If **only** the `imageUrl` value is used for a plain layer, this value is Required.<br /><br />* If `color` is defined, this `imageUrl` value is ignored. | *Both
|
||||
|
||||
#### Example of Plain MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"type": "plain",
|
||||
"options": {
|
||||
"color": "blue",
|
||||
"imageUrl": "http://example.com/background.png"
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### Named Map Layer Options
|
||||
|
||||
You can use a [Named Map]({{site.mapsapi_docs}}/guides/named-maps/) as a map layer. Note the following limitations before referencing the MapConfig options for a Named Map layer.
|
||||
|
||||
_**Limitations:**_
|
||||
|
||||
- A Named Map will not allow you to have `named` type layers inside of your template layergroup's layers definition
|
||||
- A `named` layer does not allow Named Maps from other accounts. You can only use Named Maps from the _same_ user account
|
||||
|
||||
If you are using `named` layer options as your map resource, the following configurations are required in your MapConfig file.
|
||||
|
||||
Named Layer Option | Description | Optional or Required?
|
||||
--- | ---
|
||||
`name` | A string value, the name for the Named Map to use. | Required
|
||||
`config` | An object, the replacement values for the Named Map's template placeholders. | Optional
|
||||
`auth_tokens` | Strings array, the authorized tokens in case the Named Map has auth method set to `token`. | Optional
|
||||
|
||||
#### Example of Named MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"type": "named",
|
||||
"options": {
|
||||
"name": "world_borders",
|
||||
"config": {
|
||||
"color": "#000"
|
||||
},
|
||||
"auth_tokens": ["token1", "token2"]
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### Aggregation Options
|
||||
|
||||
The data used to render tiles, or contained in the tiles (for the case of vector tiles), can be spatially [aggregated]({{site.mapsapi_docs}}/guides/named-maps/) under some circumstances.
|
||||
|
||||
An `aggregation` attribute can be used in the layer `options` to control the aggregation. A value of `false` will disable aggregation for the layer. Otherwise, an object can be passed with the following aggregation parameters:
|
||||
|
||||
Parameter|Description|Default value
|
||||
`placement`|Determines the kind of aggregated geometry generated ("point-sample", "point-grind" or "centroid").|"centroid"
|
||||
`columns`|Defines aggregated columns; each one by an "aggregate_function" ("sum", "avg", "min, "max", "mode", "count") and "aggregated_column" name.|
|
||||
`resolution`|Defines the cell-size of the spatial aggregation grid.|1 (for 256x256 cells per tile)
|
||||
`threshold`|Minimum rows in the dataset to apply aggregation.
|
||||
|
||||
#### Example of Aggregation MapConfig
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"version": "1.7.0",
|
||||
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
|
||||
"srid": 3857,
|
||||
"maxzoom": 18,
|
||||
"minzoom": 3,
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#table { marker-width: [total]; marker-fill: ramp(value, (red, green, blue), jenks); }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"aggregation": {
|
||||
"placement": "centroid",s
|
||||
"columns": {
|
||||
"value": {
|
||||
"aggregate_function": "avg",
|
||||
"aggregated_column": "value"
|
||||
},
|
||||
"total": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
}
|
||||
},
|
||||
"resolution": 2, // Aggregation cell is 2x2 pixels
|
||||
"threshold": 500000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### MapConfig Requirements
|
||||
|
||||
All of these are MapConfig requirements for [Anonymous Maps]({{site.mapsapi_docs}}/guides/anonymous-maps/#retrieve-resources-from-the-layergroup).
|
||||
|
||||
- Identified by `{z}/{x}/{y}` path
|
||||
|
||||
- If applicable, additionally identified by `LAYER_NUMBER`
|
||||
|
||||
- Can be of different formats:
|
||||
- png
|
||||
- grid.json
|
||||
- torque.json
|
||||
|
||||
- Static images/previews
|
||||
- With a center or a bounding box
|
||||
|
||||
- Attributes
|
||||
-Identified by LAYER_NUMBER and FEATURE_ID
|
||||
|
||||
**Tip:** The MapConfig file may be extended for specific uses. For example, [Windshaft-CartoDB](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/MultiLayer-API.md) defines the addition of a `stat_tag` element in the config. This extension is also covered as part of the [Named Map Layer Options](#named-map-layer-options).
|
||||
@@ -1,14 +1,16 @@
|
||||
# 1. Purpose
|
||||
## MapConfig Aggregation Extension
|
||||
|
||||
### 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.7.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.7.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
### 2. Changes over specification
|
||||
|
||||
This extension introduces a new layer options for aggregated data tile generation.
|
||||
|
||||
## 2.1 Aggregation options
|
||||
#### 2.1 Aggregation options
|
||||
|
||||
The layer options attribute is extended with a new optional `aggregation` attribute.
|
||||
The value of this attribute can be `false` to explicitly disable aggregation for the layer.
|
||||
@@ -29,7 +31,7 @@ The value of this attribute can be `false` to explicitly disable aggregation for
|
||||
// object, defines the columns of the aggregated datasets. Each property corresponds to a columns name and
|
||||
// should contain an object with two properties: "aggregate_function" (one of "sum", "max", "min", "avg", "mode" or "count"),
|
||||
// and "aggregated_column" (the name of a column of the original layer query or "*")
|
||||
// A column defined as `"_cdb_features_count": {"aggregate_function": "count", aggregated_column: "*"}`
|
||||
// A column defined as `"_cdb_feature_count": {"aggregate_function": "count", aggregated_column: "*"}`
|
||||
// is always generated in addition to the defined columns.
|
||||
// The column names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used
|
||||
// for aggregated columns, as they correspond to columns always present in the result.
|
||||
@@ -55,8 +57,8 @@ The value of this attribute can be `false` to explicitly disable aggregation for
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
### History
|
||||
|
||||
## 1.0.0
|
||||
#### 1.0.0
|
||||
|
||||
- Initial version
|
||||
@@ -1,16 +1,18 @@
|
||||
# 1. Purpose
|
||||
## MapConfig Analyses Extension
|
||||
|
||||
### 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
|
||||
### 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
|
||||
#### 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.
|
||||
@@ -37,7 +39,7 @@ Basic analyses example:
|
||||
]
|
||||
```
|
||||
|
||||
# 2.2. Integration with layers
|
||||
### 2.2. Integration with layers
|
||||
|
||||
As pointed before an analysis node id can be referenced from layers to consume its output query.
|
||||
|
||||
@@ -57,7 +59,7 @@ The layer consuming the output must reference it with the following option:
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
#### 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
@@ -86,8 +88,8 @@ The layer consuming the output must reference it with the following option:
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
### History
|
||||
|
||||
## 1.0.0
|
||||
#### 1.0.0
|
||||
|
||||
- Initial version
|
||||
@@ -1,18 +1,20 @@
|
||||
# 1. Purpose
|
||||
## MapConfig Dataviews Extension
|
||||
|
||||
### 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
|
||||
### 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: aggregated lists, aggregations, and histograms.
|
||||
|
||||
## 2.1. Dataview types
|
||||
#### 2.1. Dataview types
|
||||
|
||||
### Aggregation
|
||||
##### Aggregation
|
||||
|
||||
An aggregation is a list with aggregated results by a column and a given aggregation function.
|
||||
|
||||
@@ -56,7 +58,7 @@ Expected output
|
||||
}
|
||||
```
|
||||
|
||||
### Histograms
|
||||
##### Histograms
|
||||
|
||||
Histograms represent the data distribution for a column.
|
||||
|
||||
@@ -88,7 +90,7 @@ Expected output
|
||||
}
|
||||
```
|
||||
|
||||
### Formula
|
||||
##### Formula
|
||||
|
||||
Formulas given a final value representing the whole dataset.
|
||||
|
||||
@@ -124,7 +126,7 @@ Result
|
||||
```
|
||||
|
||||
|
||||
## 2.2 Dataviews attribute
|
||||
#### 2.2 Dataviews attribute
|
||||
|
||||
The new dataviews attribute must be a dictionary of dataviews.
|
||||
|
||||
@@ -145,7 +147,7 @@ The layer consuming the output must reference it with the following option:
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
#### 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
@@ -185,16 +187,16 @@ The layer consuming the output must reference it with the following option:
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Filters
|
||||
#### 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
|
||||
#### 3.1 Filter types
|
||||
|
||||
### Category
|
||||
##### 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.
|
||||
@@ -208,7 +210,7 @@ Params
|
||||
}
|
||||
```
|
||||
|
||||
### Range filter
|
||||
##### Range filter
|
||||
|
||||
Allows to remove results that don’t satisfy numeric min and max values.
|
||||
Filter is applied to a numeric column.
|
||||
@@ -222,13 +224,13 @@ Params
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2. How to apply filters
|
||||
#### 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
|
||||
##### Anonymous map
|
||||
|
||||
`GET /api/v1/map?config=:mapconfig&filters=:filters`
|
||||
|
||||
@@ -241,7 +243,7 @@ If in the future we need to support a bigger filters param and it doesn’t fit
|
||||
`POST /api/v1/map`
|
||||
with `BODY={“config”: :mapconfig, “filters”: :filters}`
|
||||
|
||||
### Named map
|
||||
##### Named map
|
||||
|
||||
Assume :params (a valid JSON) as named maps params, like in: `{“color”: “red”}`
|
||||
|
||||
@@ -257,7 +259,7 @@ If, again, in the future we need to support a bigger filters param that doesn’
|
||||
with `BODY={“config”: :params, “filters”: :filters}`
|
||||
|
||||
|
||||
## 3.3 Bounding box special filter
|
||||
#### 3.3 Bounding box special filter
|
||||
|
||||
A bounding box filter allows to remove results that don’t satisfy a geospatial range.
|
||||
|
||||
@@ -270,8 +272,8 @@ 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
|
||||
### History
|
||||
|
||||
## 1.0.0-alpha
|
||||
#### 1.0.0-alpha
|
||||
|
||||
- WIP document
|
||||
@@ -1,14 +1,16 @@
|
||||
# 1. Purpose
|
||||
## MapConfig Named Maps Extension
|
||||
|
||||
### 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
### 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.
|
||||
|
||||
## 2.1 Named layers definition
|
||||
#### 2.1 Named layers definition
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -42,15 +44,15 @@ This extension introduces a new layer type so it's possible to use a Named Map b
|
||||
}
|
||||
```
|
||||
|
||||
## 2.2 Limitations
|
||||
#### 2.2 Limitations
|
||||
|
||||
1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition.
|
||||
2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very
|
||||
same user account.
|
||||
|
||||
|
||||
# History
|
||||
### History
|
||||
|
||||
## 1.0.0
|
||||
#### 1.0.0
|
||||
|
||||
- Initial version
|
||||
@@ -1,4 +1,4 @@
|
||||
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.
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/CartoDB/Windshaft/blob/master/doc/internal/multilayer-API.md) in a few ways.
|
||||
|
||||
## Last modification timestamp embedded in the token
|
||||
|
||||
@@ -25,4 +25,4 @@ Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
## Stats tag
|
||||
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.
|
||||
1497
docs/reference/swagger.yaml
Normal file
1497
docs/reference/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
33
docs/support/01-support-options.md
Normal file
33
docs/support/01-support-options.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## Support Options
|
||||
|
||||
Feeling stuck? There are many ways to find help.
|
||||
|
||||
* Ask a question on [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/carto) using the `CARTO` tag.
|
||||
* [Report an issue](https://github.com/CartoDB/cartodb/issues) in Github.
|
||||
* Engine Plan customers have additional access to enterprise-level support through CARTO's support representatives.
|
||||
|
||||
If you just want to describe an issue or share an idea, just <a class="typeform-share" href="https://cartohq.typeform.com/to/mH6RRl" data-mode="popup" target="_blank"> send your feedback</a>
|
||||
|
||||
### Issues on Github
|
||||
|
||||
If you think you may have found a bug, or if you have a feature request that you would like to share with the Maps API team, please [open an issue](https://github.com/CartoDB/Windshaft-cartodb/issues/new).
|
||||
|
||||
### Community support on GIS Stack Exchange
|
||||
|
||||
GIS Stack Exchange is the most popular community in the geospatial industry. This is a collaboratively-edited question and answer site for geospatial programmers and technicians. It is a fantastic resource for asking technical questions about developing and maintaining your application.
|
||||
|
||||
|
||||
When posting a new question, please consider the following:
|
||||
|
||||
* Read the GIS Stack Exchange [help](https://gis.stackexchange.com/help) and [how to ask](https://gis.stackexchange.com/help/how-to-ask) pages for guidelines and tips about posting questions.
|
||||
* Be very clear about your question in the subject. A clear explanation helps those trying to answer your question, as well as those who may be looking for information in the future.
|
||||
* Be informative in your post. Details, code snippets, logs, screenshots, etc. help others to understand your problem.
|
||||
* Use code that demonstrates the problem. It is very hard to debug errors without sample code to reproduce the problem.
|
||||
|
||||
### Engine Plan Customers
|
||||
|
||||
Engine Plan customers have additional support options beyond general community support. As per your account Terms of Service, you have access to enterprise-level support through CARTO's support representatives available at [enterprise-support@carto.com](mailto:enterprise-support@carto.com)
|
||||
|
||||
In order to speed up the resolution of your issue, provide as much information as possible (even if it is a link from community support). This allows our engineers to investigate your problem as soon as possible.
|
||||
|
||||
If you are not yet CARTO customer, browse our [plans & pricing](https://carto.com/pricing/) and find the right plan for you.
|
||||
36
docs/support/02-contribute.md
Normal file
36
docs/support/02-contribute.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Contribute
|
||||
|
||||
CARTO platform is an open-source ecosystem. You can read about the [fundamentals]({{site.fundamental_docs}}/components/) of CARTO architecture and its components.
|
||||
We are more than happy to receive your contributions to the code and the documentation as well.
|
||||
|
||||
## Filling a ticket
|
||||
|
||||
If you want to open a new issue in our repository, please follow these instructions:
|
||||
|
||||
1. Descriptive title.
|
||||
2. Write a good description, it always helps.
|
||||
3. Specify the steps to reproduce the problem.
|
||||
4. Try to add an example showing the problem.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Best part of open source, collaborate in Maps API code!. We like hearing from you, so if you have any bug fixed, or a new feature ready to be merged, those are the steps you should follow:
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a new branch in your forked repository.
|
||||
3. Commit your changes. Add new tests if it is necessary.
|
||||
4. Open a pull request.
|
||||
5. Any of the maintainers will take a look.
|
||||
6. If everything works, it will merged and released \o/.
|
||||
|
||||
If you want more detailed information, this [GitHub guide](https://guides.github.com/activities/contributing-to-open-source/) is a must.
|
||||
|
||||
## Completing documentation
|
||||
|
||||
Maps API documentation is located in ```docs/```. That folder is the content that appears in the [Developer Center](https://carto.com/developers/maps-api/). Just follow the instructions described in [contributing code](#contributing-code) and after accepting your pull request, we will make it appear online :).
|
||||
|
||||
**Tip:** A convenient, easy way of proposing changes in documentation is by using the GitHub editor directly on the web. You can easily create a branch with your changes and make a PR from there.
|
||||
|
||||
## Submitting contributions
|
||||
|
||||
You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://carto.com/contributions).
|
||||
121
docs/support/03-rate-limiting.md
Normal file
121
docs/support/03-rate-limiting.md
Normal file
@@ -0,0 +1,121 @@
|
||||
## Rate limiting
|
||||
|
||||
Rate limits ensure that CARTO platform is not flooded with so many requests it does not have the time and resources to service them all.
|
||||
|
||||
Of course, there is nothing we can do to prevent people from actually sending as many requests to our platform as they want, but requests over a user's rate limit will be acknowledged with an error so that the sender understands they need to lower the rate at which requests are sent before they are serviced again.
|
||||
|
||||
Currently, Maps API is affected by rate limiting.
|
||||
|
||||
### Per user and endpoint
|
||||
|
||||
Rate limit is on a per-user basis (or more accurately described, per user access) and by endpoint. For example, suppose you have 2 different apps (with 2 different maps) and both call to the same endpoint that allows 100 requests per second. Both apps/maps "share" 100 requests per second regardless the map calling to this endpoint.
|
||||
|
||||
|
||||
### How it works
|
||||
|
||||
We are using the [generic cell rate algorithm](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm), a [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm type.
|
||||
|
||||
The main keys to keep in mind about this algorithm and our implementation are:
|
||||
- We allow a request every a certain time period
|
||||
```
|
||||
If an endpoint has a limit of 5 requests per second, you will have a request available every 200ms and when you spend all the available requests, you will need to wait 200ms to have another available request, instead of 1 second
|
||||
```
|
||||
- Most of the endpoints are limited per second
|
||||
```
|
||||
If an endpoint has a limit of 5 requests per second, after a second without requests, you will have at least 5 available requests
|
||||
```
|
||||
- Most of the endpoints allow an initial burst equal to the number of requests per second
|
||||
```
|
||||
If an endpoint has a limit of 5 requests per second, initially you will have 5 available requests
|
||||
```
|
||||
|
||||
### Caches
|
||||
|
||||
In computing, a cache is a high-speed data storage layer which stores data, typically a set of data, so that future requests for that data are served up faster than by accessing the original location.
|
||||
|
||||
CARTO caching allows you to efficiently reuse previously retrieved or computed data, as the data in a cache is stored by CARTO in fast access hardware in combination with specific software to manage this.
|
||||
|
||||
Resources accessed by caches don't count against the limits. That is, any request that is handled by any cache layer is out of limits. You can always know which resources are served through cache looking at the `X-Cache` HTTP Header.
|
||||
|
||||
|
||||
### HTTP Headers and Response Codes
|
||||
|
||||
When an application exceeds the rate limit for a given API endpoint, the API will return an HTTP `429 Too Many Requests` error.
|
||||
|
||||
Use the HTTP headers in order to understand where the application is at for a given rate limit, on the method that was just utilized. Note that the HTTP headers are contextual. That is, they indicate the rate limit for the user context. If you have multiple apps (maps) accessing to their resources with the same user, HTTP headers are related to that user.
|
||||
|
||||
- **Carto-Rate-Limit-Limit**: total allowed requests
|
||||
- **Carto-Rate-Limit-Remaining**: remaining requests
|
||||
- **Retry-After**: seconds until next available request (returns `-1` if the current request is allowed)
|
||||
- **Carto-Rate-Limit-Reset**: seconds until the limit will reset to its maximum capacity
|
||||
|
||||
### Tips
|
||||
|
||||
We only have 1 tip:
|
||||
- If you receive a rate limit error, you must wait the seconds indicated by the `Retry-After` HTTP header (most of the time will be 1 second)
|
||||
|
||||
### Rate Limits Chart
|
||||
|
||||
Below, you can find the values of the rate limit by user account type and endpoint. Note that endpoints not listed in the chart are disabled by default.
|
||||
|
||||
#### Enterprise plans
|
||||
|
||||
|Endpoint |Request |Time period |Burst |
|
||||
| :--- | ---: | ---: | ---: |
|
||||
| GET /api/v1/map <br> POST /api/v1/map |10 |1 |10 |
|
||||
| GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format} <br> GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format} |3 |1 |3 |
|
||||
| GET /api/v1/map/static/named/{template_id}/{width}/{height}.{format} |3 |1 |3 |
|
||||
| GET /api/v1/map/{token}/{layer}/widget/{dataviewName} <br> GET /api/v1/map/{token}/dataview/{dataviewName} |25 |1 |25 |
|
||||
| GET /{token}/{layer}/widget/{dataviewName}/search <br> GET /{token}/dataview/{dataviewName}/search |3 |1 |3 |
|
||||
| GET /api/v1/map/{token}/analysis/node/{nodeId} |3 |1 |3 |
|
||||
| GET /api/v1/map/{token}/{z}/{x}/{y}@{scale_factor}?x.{format} <br> GET /api/v1/map/{token}/{z}/{x}/{y}.{format} <br> GET /api/v1/map/{token}/{layer}/{z}/{x}/{y}.{format} |120<br> 1500 |1<br> 60 |120<br> 750 |
|
||||
| GET /api/v1/map/{token}/{layer}/attributes/{fid} |10 |1 |10 |
|
||||
| GET /api/v1/map/named |3 |1 |3 |
|
||||
| POST /api/v1/map/named |3 |1 |3 |
|
||||
| GET /api/v1/map/named/{template_id} |10 |1 |10 |
|
||||
| POST /api/v1/map/named/{template_id} <br> GET /api/v1/map/named/{template_id}/jsonp |10 |1 |10 |
|
||||
| PUT /api/v1/map/named/{template_id} |10 |1 |10 |
|
||||
| DELETE /api/v1/map/named/{template_id} |3 |1 |3 |
|
||||
| GET /api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.{format} |25 |1 |25 |
|
||||
|
||||
|
||||
#### Individual plans
|
||||
|
||||
|Endpoint |Request |Time period |Burst |
|
||||
| :--- | ---: | ---: | ---: |
|
||||
| GET /api/v1/map <br> POST /api/v1/map |5 |1 |5 |
|
||||
| GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format} <br> GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format} |1 |1 |1 |
|
||||
| GET /api/v1/map/static/named/{template_id}/{width}/{height}.{format} |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/{layer}/widget/{dataviewName} <br> GET /api/v1/map/{token}/dataview/{dataviewName} |15 |1 |15 |
|
||||
| GET /{token}/{layer}/widget/{dataviewName}/search <br> GET /{token}/dataview/{dataviewName}/search |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/analysis/node/{nodeId} |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/{z}/{x}/{y}@{scale_factor}?x.{format} <br> GET /api/v1/map/{token}/{z}/{x}/{y}.{format} <br> GET /api/v1/map/{token}/{layer}/{z}/{x}/{y}.{format} |40<br> 600 |1<br> 60 |40<br> 300 |
|
||||
| GET /api/v1/map/{token}/{layer}/attributes/{fid} |5 |1 |5 |
|
||||
| GET /api/v1/map/named |1 |1 |1 |
|
||||
| POST /api/v1/map/named |1 |1 |1 |
|
||||
| GET /api/v1/map/named/{template_id} |5 |1 |5 |
|
||||
| POST /api/v1/map/named/{template_id} <br> GET /api/v1/map/named/{template_id}/jsonp |5 |1 |5 |
|
||||
| PUT /api/v1/map/named/{template_id} |5 |1 |5 |
|
||||
| DELETE /api/v1/map/named/{template_id} |1 |1 |1 |
|
||||
| GET /api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.{format} |10 |1 |10 |
|
||||
|
||||
|
||||
#### Free plans
|
||||
|
||||
|Endpoint |Request |Time period |Burst |
|
||||
| :--- | ---: | ---: | ---: |
|
||||
| GET /api/v1/map <br> POST /api/v1/map |2 |1 |2 |
|
||||
| GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format} <br> GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format} |1 |1 |1 |
|
||||
| GET /api/v1/map/static/named/{template_id}/{width}/{height}.{format} |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/{layer}/widget/{dataviewName} <br> GET /api/v1/map/{token}/dataview/{dataviewName} |10 |1 |10 |
|
||||
| GET /{token}/{layer}/widget/{dataviewName}/search <br> GET /{token}/dataview/{dataviewName}/search |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/analysis/node/{nodeId} |1 |1 |1 |
|
||||
| GET /api/v1/map/{token}/{z}/{x}/{y}@{scale_factor}?x.{format} <br> GET /api/v1/map/{token}/{z}/{x}/{y}.{format} <br> GET /api/v1/map/{token}/{layer}/{z}/{x}/{y}.{format} |20<br> 600 |1<br> 60 |20<br> 300 |
|
||||
| GET /api/v1/map/{token}/{layer}/attributes/{fid} |2 |1 |2 |
|
||||
| GET /api/v1/map/named |1 |1 |1 |
|
||||
| POST /api/v1/map/named |1 |1 |1 |
|
||||
| GET /api/v1/map/named/{template_id} |2 |1 |2 |
|
||||
| POST /api/v1/map/named/{template_id} <br> GET /api/v1/map/named/{template_id}/jsonp |2 |1 |2 |
|
||||
| PUT /api/v1/map/named/{template_id} |2 |1 |2 |
|
||||
| DELETE /api/v1/map/named/{template_id} |1 |1 |1 |
|
||||
| GET /api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.{format} |10 |1 |10 |
|
||||
32
docs/support/04-timeout-limiting.md
Normal file
32
docs/support/04-timeout-limiting.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Timeout limit
|
||||
|
||||
Our APIs work following a request <-> response model. While CARTO is busy getting that action done or retrieving that information, part of our infrastructure is devoted to that process and is therefore unavailable for any other user. Typically this is not a problem, as most requests get serviced quickly enough. However, certain requests can take a long time to process, either by design (e.g., updating a huge table) or by mistake. To prevent this long-running queries from effectively blocking the usage of our platform resources, CARTO will discard requests that cannot be fulfilled in less than a certain amount of time.
|
||||
|
||||
Maps API is affected by this kind of limiting.
|
||||
|
||||
### Per User
|
||||
|
||||
Timeout limit is on a per-user basis (or more accurately described, per user access).
|
||||
|
||||
### How it works
|
||||
|
||||
Every query has a statement timeout. When a request reaches that value, the response returns an error.
|
||||
|
||||
### Response Codes
|
||||
|
||||
When query exceeds the timeout limit, the API will return an HTTP `429 Too Many Requests` error.
|
||||
|
||||
### Tips
|
||||
|
||||
You are able to avoid common issues that trigger timeout limits following these actions:
|
||||
|
||||
- Always use database indexes
|
||||
- Try to use batch API to insert/update/delete data
|
||||
|
||||
### Timeout Limits Chart
|
||||
|
||||
Below, you can find the values of the timeout limit by user account type.
|
||||
|
||||
|Enterprise plans |Individual plans |Free plans |
|
||||
| --- | --- | --- |
|
||||
| 25 seconds | 15 seconds | 5 seconds |
|
||||
16
docs/support/05-quota-limiting.md
Normal file
16
docs/support/05-quota-limiting.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## Quota limiting
|
||||
|
||||
CARTO platform imposes limits on how much data you can store at CARTO, for every user account and organization. You can learn more about this topic by reading the [fundamentals about limits]({{site.fundamental_docs}}/limits/of the CARTO platform.
|
||||
|
||||
Maps API is affected by this kind of limiting.
|
||||
|
||||
### Quota Limits Chart
|
||||
|
||||
Below, you can find the values of the different quota limits by user account type.
|
||||
|
||||
|Limit |Enterprise plans |Individual plans |Free plans |
|
||||
| :--- | ---: | ---: | ---: |
|
||||
| Maximum Static Map image size |4000 X 4000 pixels |4000 X 4000 pixels |4000 X 4000 pixels |
|
||||
| Maximum number of Named Maps |4096 |4096 |4096 |
|
||||
| Maximum number of layers |10 |8 |8 |
|
||||
| Maximum number of layers |16 |8 |8 |
|
||||
@@ -1,9 +1,10 @@
|
||||
Windshaft-cartodb metrics
|
||||
=========================
|
||||
See [Windshaft metrics documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
|
||||
## Metrics
|
||||
|
||||
See [metrics guide](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
|
||||
|
||||
The next list includes the API endpoints, each endpoint may have several inner timers, some of them are displayed within this list as subitems. Find the description for them in the Inner timers section.
|
||||
## Timers
|
||||
|
||||
### Timers
|
||||
- **windshaft-cartodb.flush_cache**: time to flush the tile and sql cache
|
||||
- **windshaft-cartodb.get_template**: time to retrieve an specific template
|
||||
- **windshaft-cartodb.delete_template**: time to delete an specific template
|
||||
@@ -39,4 +40,3 @@ Again, each inner timer may have several inner timers.
|
||||
- **setDBConn**: time to retrieve from redis and set db host and db name from a user
|
||||
- **setDBParams**: time to prepare all db params to be able to connect/query a database, see *setDBAuth* and *setDBConn*
|
||||
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user
|
||||
|
||||
271
lib/api/api-router.js
Normal file
271
lib/api/api-router.js
Normal file
@@ -0,0 +1,271 @@
|
||||
'use strict';
|
||||
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const RedisPool = require('redis-mpool');
|
||||
const cartodbRedis = require('cartodb-redis');
|
||||
|
||||
const { factory: windshaftFactory } = require('windshaft');
|
||||
|
||||
const PgConnection = require('../backends/pg-connection');
|
||||
const AnalysisBackend = require('../backends/analysis');
|
||||
const AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
const DataviewBackend = require('../backends/dataview');
|
||||
const TemplateMaps = require('../backends/template-maps');
|
||||
const PgQueryRunner = require('../backends/pg-query-runner');
|
||||
const StatsBackend = require('../backends/stats');
|
||||
const AuthBackend = require('../backends/auth');
|
||||
|
||||
const UserLimitsBackend = require('../backends/user-limits');
|
||||
const OverviewsMetadataBackend = require('../backends/overviews-metadata');
|
||||
const FilterStatsApi = require('../backends/filter-stats');
|
||||
const TablesExtentBackend = require('../backends/tables-extent');
|
||||
const ClusterBackend = require('../backends/cluster');
|
||||
const PubSubMetricsBackend = require('../backends/metrics');
|
||||
|
||||
const LayergroupAffectedTablesCache = require('../cache/layergroup-affected-tables');
|
||||
const SurrogateKeysCache = require('../cache/surrogate-keys-cache');
|
||||
const VarnishHttpCacheBackend = require('../cache/backend/varnish-http');
|
||||
const FastlyCacheBackend = require('../cache/backend/fastly');
|
||||
const NamedMapProviderCache = require('../cache/named-map-provider-cache');
|
||||
const NamedMapsCacheEntry = require('../cache/model/named-maps-entry');
|
||||
const NamedMapProviderCacheReporter = require('../stats/reporter/named-map-provider-cache');
|
||||
|
||||
const SqlWrapMapConfigAdapter = require('../models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
const MapConfigNamedLayersAdapter = require('../models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
const MapConfigBufferSizeAdapter = require('../models/mapconfig/adapter/mapconfig-buffer-size-adapter');
|
||||
const AnalysisMapConfigAdapter = require('../models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
const MapConfigOverviewsAdapter = require('../models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
const TurboCartoAdapter = require('../models/mapconfig/adapter/turbo-carto-adapter');
|
||||
const DataviewsWidgetsAdapter = require('../models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
const AggregationMapConfigAdapter = require('../models/mapconfig/adapter/aggregation-mapconfig-adapter');
|
||||
const MapConfigAdapter = require('../models/mapconfig/adapter');
|
||||
const VectorMapConfigAdapter = require('../models/mapconfig/adapter/vector-mapconfig-adapter');
|
||||
|
||||
const ResourceLocator = require('../models/resource-locator');
|
||||
const LayergroupMetadata = require('../utils/layergroup-metadata');
|
||||
const RendererStatsReporter = require('../stats/reporter/renderer');
|
||||
|
||||
const initializeStatusCode = require('./middlewares/initialize-status-code');
|
||||
const initLogger = require('./middlewares/logger');
|
||||
const bodyParser = require('body-parser');
|
||||
const servedByHostHeader = require('./middlewares/served-by-host-header');
|
||||
const profiler = require('./middlewares/profiler');
|
||||
const lzmaMiddleware = require('./middlewares/lzma');
|
||||
const cors = require('./middlewares/cors');
|
||||
const user = require('./middlewares/user');
|
||||
const sendResponse = require('./middlewares/send-response');
|
||||
const syntaxError = require('./middlewares/syntax-error');
|
||||
const errorMiddleware = require('./middlewares/error-middleware');
|
||||
const clientHeader = require('./middlewares/client-header');
|
||||
|
||||
const MapRouter = require('./map/map-router');
|
||||
const TemplateRouter = require('./template/template-router');
|
||||
|
||||
const getOnTileErrorStrategy = require('../utils/on-tile-error-strategy');
|
||||
|
||||
module.exports = class ApiRouter {
|
||||
constructor ({ serverOptions, environmentOptions }) {
|
||||
this.serverOptions = serverOptions;
|
||||
|
||||
const redisOptions = Object.assign({
|
||||
name: 'windshaft-server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}, environmentOptions.redis);
|
||||
|
||||
const redisPool = new RedisPool(redisOptions);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
const { rendererCache, tileBackend, attributesBackend, previewBackend, mapBackend, mapStore } = windshaftFactory({
|
||||
rendererOptions: serverOptions,
|
||||
redisPool,
|
||||
onTileErrorStrategy: getOnTileErrorStrategy({ enabled: environmentOptions.enabledFeatures.onTileErrorStrategy }),
|
||||
logger: this.serverOptions.logger
|
||||
});
|
||||
|
||||
const rendererStatsReporter = new RendererStatsReporter(rendererCache, serverOptions.renderCache.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
const metadataBackend = cartodbRedis({ pool: redisPool });
|
||||
const pgConnection = new PgConnection(metadataBackend);
|
||||
|
||||
const surrogateKeysCacheBackends = createSurrogateKeysCacheBackends(serverOptions);
|
||||
const surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
|
||||
const templateMaps = createTemplateMaps({ redisPool, surrogateKeysCache, logger: this.serverOptions.logger });
|
||||
|
||||
const analysisStatusBackend = new AnalysisStatusBackend();
|
||||
const analysisBackend = new AnalysisBackend(metadataBackend, serverOptions.analysis);
|
||||
const dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
const statsBackend = new StatsBackend();
|
||||
const clusterBackend = new ClusterBackend();
|
||||
|
||||
const userLimitsBackend = new UserLimitsBackend(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0,
|
||||
rateLimitsEnabled: global.environment.enabledFeatures.rateLimitsEnabled
|
||||
}
|
||||
});
|
||||
const authBackend = new AuthBackend(pgConnection, metadataBackend, mapStore, templateMaps);
|
||||
|
||||
const layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
}
|
||||
|
||||
const pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
const overviewsMetadataBackend = new OverviewsMetadataBackend(pgQueryRunner);
|
||||
|
||||
const filterStatsBackend = new FilterStatsApi(pgQueryRunner);
|
||||
const tablesExtentBackend = new TablesExtentBackend(pgQueryRunner);
|
||||
|
||||
const mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new MapConfigBufferSizeAdapter(),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new VectorMapConfigAdapter(pgConnection),
|
||||
new AggregationMapConfigAdapter(pgConnection),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
const resourceLocator = new ResourceLocator(global.environment);
|
||||
const layergroupMetadata = new LayergroupMetadata(resourceLocator);
|
||||
|
||||
const namedMapProviderCache = new NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
layergroupAffectedTablesCache
|
||||
);
|
||||
|
||||
const namedMapProviderCacheReporter = new NamedMapProviderCacheReporter({
|
||||
namedMapProviderCache,
|
||||
intervalInMilliseconds: serverOptions.renderCache.statsInterval
|
||||
});
|
||||
namedMapProviderCacheReporter.start();
|
||||
|
||||
const metricsBackend = new PubSubMetricsBackend(serverOptions.pubSubMetrics);
|
||||
|
||||
const collaborators = {
|
||||
config: serverOptions,
|
||||
analysisStatusBackend,
|
||||
attributesBackend,
|
||||
dataviewBackend,
|
||||
previewBackend,
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tablesExtentBackend,
|
||||
clusterBackend,
|
||||
metricsBackend
|
||||
};
|
||||
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.mapRouter = new MapRouter({ collaborators });
|
||||
this.templateRouter = new TemplateRouter({ collaborators });
|
||||
}
|
||||
|
||||
route (app, routes) {
|
||||
// FIXME: we need a better way to reset cache while running tests
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
app.layergroupAffectedTablesCache = this.layergroupAffectedTablesCache;
|
||||
}
|
||||
|
||||
routes.forEach(route => {
|
||||
const apiRouter = router({ mergeParams: true });
|
||||
const { paths, middlewares = [] } = route;
|
||||
|
||||
apiRouter.use(initLogger({ logger: this.serverOptions.logger }));
|
||||
apiRouter.use(user(this.metadataBackend));
|
||||
apiRouter.use(profiler({
|
||||
enabled: this.serverOptions.useProfiler,
|
||||
statsClient: global.statsClient
|
||||
}));
|
||||
|
||||
middlewares.forEach(middleware => apiRouter.use(middleware()));
|
||||
|
||||
apiRouter.use(initializeStatusCode());
|
||||
apiRouter.use(bodyParser.json());
|
||||
apiRouter.use(servedByHostHeader());
|
||||
apiRouter.use(clientHeader());
|
||||
apiRouter.use(lzmaMiddleware());
|
||||
apiRouter.use(cors());
|
||||
|
||||
this.templateRouter.route(apiRouter, route.template);
|
||||
this.mapRouter.route(apiRouter, route.map);
|
||||
|
||||
apiRouter.use(sendResponse());
|
||||
apiRouter.use(syntaxError());
|
||||
apiRouter.use(errorMiddleware());
|
||||
|
||||
paths.forEach(path => app.use(path, apiRouter));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function createTemplateMaps ({ redisPool, surrogateKeysCache, logger }) {
|
||||
const templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
function invalidateNamedMap (user, templateName) {
|
||||
const startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(user, templateName), (err) => {
|
||||
if (err) {
|
||||
return logger.error({ exception: err, 'cdb-user': user, template_id: templateName }, 'Named map invalidation failed');
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
logger.info({ 'cdb-user': user, template_id: templateName, duration: elapsed / 1000, duration_ms: elapsed }, 'Named map invalidation success');
|
||||
});
|
||||
}
|
||||
|
||||
['update', 'delete'].forEach(function (eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
return templateMaps;
|
||||
}
|
||||
|
||||
function createSurrogateKeysCacheBackends (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;
|
||||
}
|
||||
148
lib/api/map/analyses-catalog-controller.js
Normal file
148
lib/api/map/analyses-catalog-controller.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
const PSQL = require('cartodb-psql');
|
||||
const tag = require('../middlewares/tag');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const dbParamsFromResLocals = require('../../utils/database-params');
|
||||
|
||||
module.exports = class AnalysesController {
|
||||
constructor (pgConnection, authBackend, userLimitsBackend) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/analyses/catalog', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['analysis', 'catalog'] }),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS_CATALOG),
|
||||
cleanUpQueryParams(),
|
||||
createPGClient(),
|
||||
getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
prepareResponse(),
|
||||
cacheControlHeader({ ttl: 10, revalidate: true }),
|
||||
unauthorizedError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function createPGClient () {
|
||||
return function createPGClientMiddleware (req, res, next) {
|
||||
const dbParams = dbParamsFromResLocals(res.locals);
|
||||
|
||||
res.locals.pg = new PSQL(dbParams);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function getDataFromQuery ({ queryTemplate, key }) {
|
||||
const readOnlyTransactionOn = true;
|
||||
|
||||
return function getCatalogMiddleware (req, res, next) {
|
||||
const { pg, user } = res.locals;
|
||||
const sql = queryTemplate({ _username: user });
|
||||
|
||||
pg.query(sql, (err, resultSet = {}) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals[key] = resultSet.rows || [];
|
||||
|
||||
next();
|
||||
}, readOnlyTransactionOn);
|
||||
};
|
||||
}
|
||||
|
||||
function prepareResponse () {
|
||||
return function prepareResponseMiddleware (req, res, next) {
|
||||
const { catalog, tables } = res.locals;
|
||||
|
||||
const analysisIdToTable = tables.reduce((analysisIdToTable, table) => {
|
||||
const analysisId = table.relname.split('_')[2];
|
||||
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
const analysisCatalog = catalog.map(analysis => {
|
||||
if (Object.prototype.hasOwnProperty.call(analysisIdToTable, analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
|
||||
return analysis;
|
||||
})
|
||||
.sort((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;
|
||||
});
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { catalog: analysisCatalog };
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function unauthorizedError () {
|
||||
return function unathorizedErrorMiddleware (err, req, res, next) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
const catalogQueryTpl = ctx => `
|
||||
SELECT analysis_def->>'type' as type, * FROM cartodb.cdb_analysis_catalog WHERE username = '${ctx._username}'
|
||||
`;
|
||||
|
||||
var tablesQueryTpl = ctx => `
|
||||
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 ('${ctx._username}', 'public')
|
||||
)
|
||||
SELECT *, pg_size_pretty(size) as size_pretty
|
||||
FROM analysis_tables
|
||||
ORDER BY size DESC
|
||||
`;
|
||||
63
lib/api/map/analysis-layergroup-controller.js
Normal file
63
lib/api/map/analysis-layergroup-controller.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const dbParamsFromResLocals = require('../../utils/database-params');
|
||||
|
||||
module.exports = class AnalysisLayergroupController {
|
||||
constructor (analysisStatusBackend, pgConnection, userLimitsBackend, authBackend) {
|
||||
this.analysisStatusBackend = analysisStatusBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.authBackend = authBackend;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/:token/analysis/node/:nodeId', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['analysis', 'node'] }),
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS),
|
||||
cleanUpQueryParams(),
|
||||
analysisNodeStatus(this.analysisStatusBackend)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function analysisNodeStatus (analysisStatusBackend) {
|
||||
return function analysisNodeStatusMiddleware (req, res, next) {
|
||||
const { nodeId } = req.params;
|
||||
const dbParams = dbParamsFromResLocals(res.locals);
|
||||
|
||||
analysisStatusBackend.getNodeStatus(nodeId, dbParams, (err, nodeStatus, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET NODE STATUS';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = nodeStatus;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
242
lib/api/map/anonymous-map-controller.js
Normal file
242
lib/api/map/anonymous-map-controller.js
Normal file
@@ -0,0 +1,242 @@
|
||||
'use strict';
|
||||
|
||||
const windshaft = require('windshaft');
|
||||
const MapConfig = windshaft.model.MapConfig;
|
||||
const Datasource = windshaft.model.Datasource;
|
||||
const tag = require('../middlewares/tag');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const checkJsonContentType = require('../middlewares/check-json-content-type');
|
||||
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
|
||||
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
|
||||
const layerStats = require('../middlewares/layer-stats');
|
||||
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
|
||||
const layergroupMetadata = require('../middlewares/layergroup-metadata');
|
||||
const mapError = require('../middlewares/map-error');
|
||||
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const metrics = require('../middlewares/metrics');
|
||||
|
||||
module.exports = class AnonymousMapController {
|
||||
/**
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsBackend} userLimitsBackend
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
constructor (
|
||||
config,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTables,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
metricsBackend
|
||||
) {
|
||||
this.config = config;
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.statsBackend = statsBackend;
|
||||
this.authBackend = authBackend;
|
||||
this.layergroupMetadata = layergroupMetadata;
|
||||
this.metricsBackend = metricsBackend;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.options('/');
|
||||
mapRouter.get('/', this.middlewares());
|
||||
mapRouter.post('/', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
const useTemplateHash = false;
|
||||
const includeQuery = true;
|
||||
const label = 'ANONYMOUS LAYERGROUP';
|
||||
const addContext = true;
|
||||
const metricsTags = {
|
||||
event: 'map_view',
|
||||
attributes: { map_type: 'anonymous' },
|
||||
from: {
|
||||
req: {
|
||||
query: { client: 'client' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
tag({ tags: ['map', 'anonymous'] }),
|
||||
metrics({
|
||||
enabled: this.config.pubSubMetrics.enabled,
|
||||
metricsBackend: this.metricsBackend,
|
||||
tags: metricsTags
|
||||
}),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS),
|
||||
cleanUpQueryParams(['aggregation']),
|
||||
checkJsonContentType(),
|
||||
checkCreateLayergroup(),
|
||||
prepareAdapterMapConfig(this.mapConfigAdapter),
|
||||
createLayergroup(
|
||||
this.mapBackend,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
incrementMapViewCount(this.metadataBackend),
|
||||
augmentLayergroupData(),
|
||||
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
lastUpdatedTimeLayergroup(),
|
||||
layerStats(this.pgConnection, this.statsBackend),
|
||||
layergroupIdHeader(this.templateMaps, useTemplateHash),
|
||||
layergroupMetadata(this.layergroupMetadata, includeQuery),
|
||||
mapError({ label, addContext })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkCreateLayergroup () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = req.query;
|
||||
|
||||
if (!config) {
|
||||
return next(new Error('layergroup GET needs a "config" parameter'));
|
||||
}
|
||||
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
function prepareAdapterMapConfig (mapConfigAdapter) {
|
||||
return function prepareAdapterMapConfigMiddleware (req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
|
||||
const { logger } = res.locals;
|
||||
const { user, api_key: apiKey } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
const context = {
|
||||
analysisConfiguration: {
|
||||
user,
|
||||
logger,
|
||||
db: {
|
||||
host: dbhost,
|
||||
port: dbport,
|
||||
dbname: dbname,
|
||||
user: dbuser,
|
||||
pass: dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: user,
|
||||
apiKey
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mapConfigAdapter.getMapConfig(user,
|
||||
requestMapConfig,
|
||||
params,
|
||||
context,
|
||||
(err, requestMapConfig) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.body = requestMapConfig;
|
||||
res.locals.context = context;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
|
||||
return function createLayergroupMiddleware (req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
|
||||
const { context } = res.locals;
|
||||
const { user, cache_buster: cacheBuster, api_key: apiKey } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = {
|
||||
cache_buster: cacheBuster,
|
||||
api_key: apiKey,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport
|
||||
};
|
||||
|
||||
const datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
const mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
|
||||
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
|
||||
mapConfig,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
params
|
||||
);
|
||||
|
||||
res.locals.mapConfig = mapConfig;
|
||||
res.locals.mapConfigProvider = mapConfigProvider;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
const mapParams = { dbuser, dbname, dbpassword, dbhost, dbport };
|
||||
|
||||
mapBackend.createLayergroup(mapConfig, mapParams, mapConfigProvider, (err, layergroup, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = layergroup;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
96
lib/api/map/attributes-layergroup-controller.js
Normal file
96
lib/api/map/attributes-layergroup-controller.js
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
|
||||
module.exports = class AttributesLayergroupController {
|
||||
constructor (
|
||||
attributesBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/:token/:layer/attributes/:fid', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['attributes'] }),
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
|
||||
cleanUpQueryParams(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
getFeatureAttributes(this.attributesBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getFeatureAttributes (attributesBackend) {
|
||||
return function getFeatureAttributesMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { token } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { layer, fid } = req.params;
|
||||
|
||||
const params = {
|
||||
token,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport,
|
||||
layer,
|
||||
fid
|
||||
};
|
||||
|
||||
attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
102
lib/api/map/clustered-features-layergroup-controller.js
Normal file
102
lib/api/map/clustered-features-layergroup-controller.js
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
|
||||
module.exports = class AggregatedFeaturesLayergroupController {
|
||||
constructor (
|
||||
clusterBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.clusterBackend = clusterBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/:token/:layer/:z/cluster/:clusterId', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['cluster'] }),
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
// TODO: create its rate limit
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
|
||||
cleanUpQueryParams(['aggregation']),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
getClusteredFeatures(this.clusterBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getClusteredFeatures (clusterBackend) {
|
||||
return function getFeatureAttributesMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { user, token } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { layer, z: zoom, clusterId } = req.params;
|
||||
const { aggregation } = req.query;
|
||||
|
||||
const params = {
|
||||
user,
|
||||
token,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport,
|
||||
layer,
|
||||
zoom,
|
||||
clusterId,
|
||||
aggregation
|
||||
};
|
||||
|
||||
clusterBackend.getClusterFeatures(mapConfigProvider, params, (err, features, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET CLUSTERED FEATURES';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
const { rows, fields } = features;
|
||||
res.body = { rows, fields };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
148
lib/api/map/dataview-layergroup-controller.js
Normal file
148
lib/api/map/dataview-layergroup-controller.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
|
||||
const ALLOWED_DATAVIEW_QUERY_PARAMS = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'circle', // json
|
||||
'polygon', // json
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
'bins', // number
|
||||
'aggregation', // string
|
||||
'offset', // number
|
||||
'q', // widgets search
|
||||
'categories' // number
|
||||
];
|
||||
|
||||
module.exports = class DataviewLayergroupController {
|
||||
constructor (
|
||||
dataviewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.dataviewBackend = dataviewBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
|
||||
mapRouter.get('/:token/dataview/:dataviewName', this.middlewares({
|
||||
action: 'get',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/:layer/widget/:dataviewName', this.middlewares({
|
||||
action: 'get',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/dataview/:dataviewName/search', this.middlewares({
|
||||
action: 'search',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/:layer/widget/:dataviewName/search', this.middlewares({
|
||||
action: 'search',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ action, rateLimitGroup }) {
|
||||
return [
|
||||
tag({ tags: ['dataview', action] }),
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, rateLimitGroup),
|
||||
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
action === 'search' ? dataviewSearch(this.dataviewBackend) : getDataview(this.dataviewBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getDataview (dataviewBackend) {
|
||||
return function getDataviewMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
const { dataviewName } = req.params;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = Object.assign({ dataviewName, dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = dataview;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function dataviewSearch (dataviewBackend) {
|
||||
return function dataviewSearchMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
const { dataviewName } = req.params;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = searchResult;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
156
lib/api/map/map-router.js
Normal file
156
lib/api/map/map-router.js
Normal file
@@ -0,0 +1,156 @@
|
||||
'use strict';
|
||||
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const AnalysisLayergroupController = require('./analysis-layergroup-controller');
|
||||
const AttributesLayergroupController = require('./attributes-layergroup-controller');
|
||||
const DataviewLayergroupController = require('./dataview-layergroup-controller');
|
||||
const PreviewLayergroupController = require('./preview-layergroup-controller');
|
||||
const TileLayergroupController = require('./tile-layergroup-controller');
|
||||
const AnonymousMapController = require('./anonymous-map-controller');
|
||||
const PreviewTemplateController = require('./preview-template-controller');
|
||||
const AnalysesCatalogController = require('./analyses-catalog-controller');
|
||||
const ClusteredFeaturesLayergroupController = require('./clustered-features-layergroup-controller');
|
||||
|
||||
module.exports = class MapRouter {
|
||||
constructor ({ collaborators }) {
|
||||
const {
|
||||
config,
|
||||
analysisStatusBackend,
|
||||
attributesBackend,
|
||||
dataviewBackend,
|
||||
previewBackend,
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tablesExtentBackend,
|
||||
clusterBackend,
|
||||
metricsBackend
|
||||
} = collaborators;
|
||||
|
||||
this.analysisLayergroupController = new AnalysisLayergroupController(
|
||||
analysisStatusBackend,
|
||||
pgConnection,
|
||||
userLimitsBackend,
|
||||
authBackend
|
||||
);
|
||||
|
||||
this.attributesLayergroupController = new AttributesLayergroupController(
|
||||
attributesBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.dataviewLayergroupController = new DataviewLayergroupController(
|
||||
dataviewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.previewLayergroupController = new PreviewLayergroupController(
|
||||
previewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.tileLayergroupController = new TileLayergroupController(
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.anonymousMapController = new AnonymousMapController(
|
||||
config,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
metricsBackend
|
||||
);
|
||||
|
||||
this.previewTemplateController = new PreviewTemplateController(
|
||||
config,
|
||||
namedMapProviderCache,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentBackend,
|
||||
metadataBackend,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend,
|
||||
metricsBackend
|
||||
);
|
||||
|
||||
this.analysesController = new AnalysesCatalogController(
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
);
|
||||
|
||||
this.clusteredFeaturesLayergroupController = new ClusteredFeaturesLayergroupController(
|
||||
clusterBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
}
|
||||
|
||||
route (apiRouter, routes) {
|
||||
const mapRouter = router({ mergeParams: true });
|
||||
|
||||
routes.forEach(route => {
|
||||
const { paths, middlewares = [] } = route;
|
||||
|
||||
middlewares.forEach(middleware => mapRouter.use(middleware()));
|
||||
|
||||
this.analysisLayergroupController.route(mapRouter);
|
||||
this.attributesLayergroupController.route(mapRouter);
|
||||
this.dataviewLayergroupController.route(mapRouter);
|
||||
this.previewLayergroupController.route(mapRouter);
|
||||
this.tileLayergroupController.route(mapRouter);
|
||||
this.anonymousMapController.route(mapRouter);
|
||||
this.previewTemplateController.route(mapRouter);
|
||||
this.analysesController.route(mapRouter);
|
||||
this.clusteredFeaturesLayergroupController.route(mapRouter);
|
||||
|
||||
paths.forEach(path => apiRouter.use(path, mapRouter));
|
||||
});
|
||||
}
|
||||
};
|
||||
152
lib/api/map/preview-layergroup-controller.js
Normal file
152
lib/api/map/preview-layergroup-controller.js
Normal file
@@ -0,0 +1,152 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const noop = require('../middlewares/noop');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
|
||||
|
||||
module.exports = class PreviewLayergroupController {
|
||||
constructor (
|
||||
previewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.previewBackend = previewBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/static/center/:token/:z/:lat/:lng/:width/:height.:format', this.middlewares({
|
||||
validateZoom: true,
|
||||
previewType: 'centered'
|
||||
}));
|
||||
|
||||
mapRouter.get('/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', this.middlewares({
|
||||
validateZoom: false,
|
||||
previewType: 'bbox'
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ validateZoom, previewType }) {
|
||||
const forcedFormat = 'png';
|
||||
|
||||
let getPreviewImage;
|
||||
|
||||
if (previewType === 'centered') {
|
||||
getPreviewImage = getPreviewImageByCenter;
|
||||
}
|
||||
|
||||
if (previewType === 'bbox') {
|
||||
getPreviewImage = getPreviewImageByBoundingBox;
|
||||
}
|
||||
|
||||
return [
|
||||
tag({ tags: ['static', 'tile'] }),
|
||||
layergroupToken(),
|
||||
validateZoom ? coordinates({ z: true, x: false, y: false }) : noop(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
||||
cleanUpQueryParams(['layer']),
|
||||
checkStaticImageFormat(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache,
|
||||
forcedFormat
|
||||
),
|
||||
getPreviewImage(this.previewBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getPreviewImageByCenter (previewBackend) {
|
||||
return function getPreviewImageByCenterMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const zoom = +req.params.z;
|
||||
const center = {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
};
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const options = { mapConfigProvider, format, width, height, zoom, center };
|
||||
|
||||
previewBackend.getImage(options, (err, image, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set('Content-Type', `image/${format}`);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getPreviewImageByBoundingBox (previewBackend) {
|
||||
return function getPreviewImageByBoundingBoxMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const bbox = {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
};
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const options = { mapConfigProvider, format, width, height, bbox };
|
||||
|
||||
previewBackend.getImage(options, (err, image, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set('Content-Type', `image/${format}`);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
393
lib/api/map/preview-template-controller.js
Normal file
393
lib/api/map/preview-template-controller.js
Normal file
@@ -0,0 +1,393 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const namedMapProvider = require('../middlewares/named-map-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const metrics = require('../middlewares/metrics');
|
||||
|
||||
const DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function numMapper (n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
module.exports = class PreviewTemplateController {
|
||||
constructor (
|
||||
config,
|
||||
namedMapProviderCache,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentBackend,
|
||||
metadataBackend,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend,
|
||||
metricsBackend
|
||||
) {
|
||||
this.config = config;
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentBackend = tablesExtentBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.metricsBackend = metricsBackend;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
mapRouter.get('/static/named/:template_id/:width/:height.:format', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
const metricsTags = {
|
||||
event: 'map_view',
|
||||
attributes: { map_type: 'static' },
|
||||
from: {
|
||||
req: {
|
||||
query: { client: 'client' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
tag({ tags: ['named', 'static', 'tile'] }),
|
||||
metrics({
|
||||
enabled: this.config.pubSubMetrics.enabled,
|
||||
metricsBackend: this.metricsBackend,
|
||||
tags: metricsTags
|
||||
}),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC_NAMED),
|
||||
cleanUpQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
checkStaticImageFormat(),
|
||||
namedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP',
|
||||
forcedFormat: 'png'
|
||||
}),
|
||||
getTemplate({ label: 'STATIC_VIZ_MAP' }),
|
||||
prepareLayerFilterFromPreviewLayers({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP'
|
||||
}),
|
||||
getStaticImageOptions({ tablesExtentBackend: this.tablesExtentBackend }),
|
||||
getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }),
|
||||
setContentTypeHeader(),
|
||||
incrementMapViews({ metadataBackend: this.metadataBackend }),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getTemplate ({ label }) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
|
||||
mapConfigProvider.getTemplate((err, template) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.template = template;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label }) {
|
||||
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
|
||||
const { template } = res.locals;
|
||||
const { config, auth_token: authToken } = req.query;
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach((layer, index) => {
|
||||
if (previewLayers['' + index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push('' + index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { user, token, cache_buster: cacheBuster, api_key: apiKey } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id: templateId, format } = req.params;
|
||||
|
||||
const params = {
|
||||
user,
|
||||
token,
|
||||
cache_buster: cacheBuster,
|
||||
api_key: apiKey,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport,
|
||||
template_id: templateId,
|
||||
format
|
||||
};
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
namedMapProviderCache.get(user, templateId, config, authToken, params, (err, provider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = provider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getStaticImageOptions ({ tablesExtentBackend }) {
|
||||
return function getStaticImageOptionsMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider, template } = res.locals;
|
||||
const { zoom, lon, lat, bbox } = req.query;
|
||||
const params = { zoom, lon, lat, bbox };
|
||||
|
||||
const imageOpts = getImageOptions(params, template);
|
||||
|
||||
if (imageOpts) {
|
||||
res.locals.imageOpts = imageOpts;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
|
||||
|
||||
mapConfigProvider.createAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var tables = affectedTables.tables || [];
|
||||
|
||||
if (tables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
tablesExtentBackend.getBounds(user, tables, (err, bounds) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = bounds;
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getImageOptions (params, template) {
|
||||
const { zoom, lon, lat, bbox } = params;
|
||||
|
||||
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromBoundingBox(bbox);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromTemplate(template, zoom);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromCoordinates (zoom, lon, lat) {
|
||||
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
|
||||
return {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: lon,
|
||||
lat: lat
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromTemplate (template, zoom) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+zoom)) {
|
||||
zoomCenter.zoom = +zoom;
|
||||
}
|
||||
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromBoundingBox (bbox = '') {
|
||||
var _bbox = bbox.split(',').map(numMapper);
|
||||
|
||||
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
|
||||
return {
|
||||
bbox: {
|
||||
west: _bbox[0],
|
||||
south: _bbox[1],
|
||||
east: _bbox[2],
|
||||
north: _bbox[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getImage ({ previewBackend, label }) {
|
||||
return function getImageMiddleware (req, res, next) {
|
||||
const { imageOpts, mapConfigProvider } = res.locals;
|
||||
const { zoom, center, bbox } = imageOpts;
|
||||
|
||||
let { width, height } = req.params;
|
||||
|
||||
width = +width;
|
||||
height = +height;
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
if (zoom !== undefined && center) {
|
||||
const options = { mapConfigProvider, format, width, height, zoom, center };
|
||||
|
||||
return previewBackend.getImage(options, (err, image, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
const options = { mapConfigProvider, format, width, height, bbox };
|
||||
|
||||
previewBackend.getImage(options, (err, image, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setContentTypeHeader () {
|
||||
return function setContentTypeHeaderMiddleware (req, res, next) {
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
res.set('Content-Type', `image/${format}`);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementMapViews ({ metadataBackend }) {
|
||||
return function incrementMapViewsMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider, logger } = res.locals;
|
||||
|
||||
mapConfigProvider.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.mapConfig = mapConfig;
|
||||
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function templateZoomCenter (view) {
|
||||
if (view.zoom !== undefined && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds (view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
|
||||
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bbox: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
167
lib/api/map/tile-layergroup-controller.js
Normal file
167
lib/api/map/tile-layergroup-controller.js
Normal file
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const vectorError = require('../middlewares/vector-error');
|
||||
|
||||
const SUPPORTED_FORMATS = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
|
||||
module.exports = class TileLayergroupController {
|
||||
constructor (
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.tileBackend = tileBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
route (mapRouter) {
|
||||
// REGEXP: doesn't match with `val`
|
||||
const not = (val) => `(?!${val})([^\/]+?)`; // eslint-disable-line no-useless-escape
|
||||
|
||||
// Sadly the path that matches 1 also matches with 2 so we need to tell to express
|
||||
// that performs only the middlewares of the first path that matches
|
||||
// for that we use one array to group all paths.
|
||||
mapRouter.get([
|
||||
'/:token/:z/:x/:y@:scale_factor?x.:format', // 1
|
||||
'/:token/:z/:x/:y.:format', // 2
|
||||
`/:token${not('static')}/:layer/:z/:x/:y.(:format)`
|
||||
], this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['tile'] }),
|
||||
layergroupToken(),
|
||||
coordinates(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
||||
cleanUpQueryParams(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
getTile(this.tileBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
incrementSuccessMetrics(global.statsClient),
|
||||
incrementErrorMetrics(global.statsClient),
|
||||
tileError(),
|
||||
vectorError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function parseFormat (format = '') {
|
||||
const prettyFormat = format.replace('.', '_');
|
||||
return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid';
|
||||
}
|
||||
|
||||
function getStatusCode (tile, format) {
|
||||
return tile.length === 0 && format === 'mvt' ? 204 : 200;
|
||||
}
|
||||
|
||||
function getTile (tileBackend) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { token } = res.locals;
|
||||
const { layer, z, x, y, format } = req.params;
|
||||
|
||||
const params = { token, layer, z, x, y, format };
|
||||
|
||||
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
res.statusCode = getStatusCode(tile, formatStat);
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function incrementSuccessMetrics (statsClient) {
|
||||
return function incrementSuccessMetricsMiddleware (req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.success');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.success`);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementErrorMetrics (statsClient) {
|
||||
return function incrementErrorMetricsMiddleware (err, req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.error');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.error`);
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
function tileError () {
|
||||
return function tileErrorMiddleware (err, req, res, next) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
let errMsg = err.message ? ('' + err.message) : ('' + err);
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
const matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
|
||||
if (matches) {
|
||||
errMsg = `style${matches[2]}: ${matches[1]}`;
|
||||
}
|
||||
|
||||
err.message = errMsg;
|
||||
err.label = 'TILE RENDER';
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
16
lib/api/middlewares/augment-layergroup-data.js
Normal file
16
lib/api/middlewares/augment-layergroup-data.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function augmentLayergroupData () {
|
||||
return function augmentLayergroupDataMiddleware (req, res, next) {
|
||||
const layergroup = res.body;
|
||||
|
||||
// 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);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
19
lib/api/middlewares/authorize.js
Normal file
19
lib/api/middlewares/authorize.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function authorize (authBackend) {
|
||||
return function authorizeMiddleware (req, res, next) {
|
||||
authBackend.authorize(req, res, (err, authorized) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!authorized) {
|
||||
err = new Error('Sorry, you are unauthorized (permission denied)');
|
||||
err.http_status = 403;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
26
lib/api/middlewares/cache-channel-header.js
Normal file
26
lib/api/middlewares/cache-channel-header.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setCacheChannelHeader () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { mapConfigProvider, logger } = res.locals;
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Error generating Cache Channel Header');
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
85
lib/api/middlewares/cache-control-header.js
Normal file
85
lib/api/middlewares/cache-control-header.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const ONE_MINUTE_IN_SECONDS = 60;
|
||||
const THREE_MINUTE_IN_SECONDS = 60 * 3;
|
||||
const FIVE_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 5;
|
||||
const TEN_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 10;
|
||||
const FIFTEEN_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 15;
|
||||
const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30;
|
||||
const ONE_HOUR_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 60;
|
||||
const ONE_YEAR_IN_SECONDS = ONE_HOUR_IN_SECONDS * 24 * 365;
|
||||
|
||||
const FALLBACK_TTL = global.environment.varnish.fallbackTtl || FIVE_MINUTES_IN_SECONDS;
|
||||
|
||||
const validFallbackTTL = [
|
||||
ONE_MINUTE_IN_SECONDS,
|
||||
THREE_MINUTE_IN_SECONDS,
|
||||
FIVE_MINUTES_IN_SECONDS,
|
||||
TEN_MINUTES_IN_SECONDS,
|
||||
FIFTEEN_MINUTES_IN_SECONDS,
|
||||
THIRTY_MINUTES_IN_SECONDS,
|
||||
ONE_HOUR_IN_SECONDS
|
||||
];
|
||||
|
||||
module.exports = function setCacheControlHeader ({
|
||||
ttl = ONE_YEAR_IN_SECONDS,
|
||||
fallbackTtl = FALLBACK_TTL,
|
||||
revalidate = false
|
||||
} = {}) {
|
||||
if (!validFallbackTTL.includes(fallbackTtl)) {
|
||||
const message = [
|
||||
'Invalid fallback TTL value for Cache-Control header.',
|
||||
`Got ${fallbackTtl}, expected ${validFallbackTTL.join(', ')}`
|
||||
].join(' ');
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { mapConfigProvider = { getAffectedTables: callback => callback() }, logger } = res.locals;
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Error generating Cache Control Header');
|
||||
return next();
|
||||
}
|
||||
|
||||
const directives = ['public'];
|
||||
|
||||
if (everyAffectedTableCanBeInvalidated(affectedTables)) {
|
||||
directives.push(`max-age=${ttl}`);
|
||||
} else {
|
||||
directives.push(`max-age=${computeNextTTL({ ttlInSeconds: fallbackTtl })}`);
|
||||
}
|
||||
|
||||
if (revalidate) {
|
||||
directives.push('must-revalidate');
|
||||
}
|
||||
|
||||
res.set('Cache-Control', directives.join(','));
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function everyAffectedTableCanBeInvalidated (affectedTables) {
|
||||
const skipNotUpdatedAtTables = false;
|
||||
const skipAnalysisCachedTables = true;
|
||||
|
||||
return affectedTables &&
|
||||
affectedTables.getTables(skipNotUpdatedAtTables, skipAnalysisCachedTables)
|
||||
.every(table => table.updated_at !== null);
|
||||
}
|
||||
|
||||
function computeNextTTL ({ ttlInSeconds } = {}) {
|
||||
const nowInSeconds = Math.ceil(Date.now() / 1000);
|
||||
const secondsAfterPreviousTTLStep = nowInSeconds % ttlInSeconds;
|
||||
const secondsToReachTheNextTTLStep = ttlInSeconds - secondsAfterPreviousTTLStep;
|
||||
|
||||
return secondsToReachTheNextTTLStep;
|
||||
}
|
||||
11
lib/api/middlewares/check-json-content-type.js
Normal file
11
lib/api/middlewares/check-json-content-type.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function checkJsonContentType () {
|
||||
return function checkJsonContentTypeMiddleware (req, res, next) {
|
||||
if (req.method === 'POST' && !req.is('application/json')) {
|
||||
return next(new Error('POST data must be of type application/json'));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
13
lib/api/middlewares/check-static-image-format.js
Normal file
13
lib/api/middlewares/check-static-image-format.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const VALID_IMAGE_FORMATS = ['png', 'jpg'];
|
||||
|
||||
module.exports = function checkStaticImageFormat () {
|
||||
return function checkStaticImageFormatMiddleware (req, res, next) {
|
||||
if (!VALID_IMAGE_FORMATS.includes(req.params.format)) {
|
||||
return next(new Error(`Unsupported image format "${req.params.format}"`));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
@@ -14,19 +16,16 @@ const REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
module.exports = function cleanUpQueryParamsMiddleware () {
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
module.exports = function cleanUpQueryParamsMiddleware (customQueryParams = []) {
|
||||
if (!Array.isArray(customQueryParams)) {
|
||||
throw new Error('customQueryParams must receive an Array of params');
|
||||
}
|
||||
|
||||
if (Array.isArray(res.locals.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
|
||||
}
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
const allowedQueryParams = [...REQUEST_QUERY_PARAMS_WHITELIST, ...customQueryParams];
|
||||
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
|
||||
// bring all query values onto res.locals object
|
||||
_.extend(res.locals, req.query);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
13
lib/api/middlewares/client-header.js
Normal file
13
lib/api/middlewares/client-header.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function clientHeader () {
|
||||
return function clientHeaderMiddleware (req, res, next) {
|
||||
const { client } = req.query;
|
||||
|
||||
if (client) {
|
||||
res.set('Carto-Client', client);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
43
lib/api/middlewares/coordinates.js
Normal file
43
lib/api/middlewares/coordinates.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const positiveIntegerNumberRegExp = /^\d+$/;
|
||||
const integerNumberRegExp = /^-?\d+$/;
|
||||
const invalidZoomMessage = function (zoom) {
|
||||
return `Invalid zoom value (${zoom}). It should be an integer number greather than or equal to 0`;
|
||||
};
|
||||
const invalidCoordXMessage = function (x) {
|
||||
return `Invalid coodinate 'x' value (${x}). It should be an integer number`;
|
||||
};
|
||||
const invalidCoordYMessage = function (y) {
|
||||
return `Invalid coodinate 'y' value (${y}). It should be an integer number greather than or equal to 0`;
|
||||
};
|
||||
|
||||
module.exports = function coordinates (validate = { z: true, x: true, y: true }) {
|
||||
return function coordinatesMiddleware (req, res, next) {
|
||||
const { z, x, y } = req.params;
|
||||
|
||||
if (validate.z && !positiveIntegerNumberRegExp.test(z)) {
|
||||
const err = new Error(invalidZoomMessage(z));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Negative values for x param are valid. The x param is wrapped
|
||||
if (validate.x && !integerNumberRegExp.test(x)) {
|
||||
const err = new Error(invalidCoordXMessage(x));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (validate.y && !positiveIntegerNumberRegExp.test(y)) {
|
||||
const err = new Error(invalidCoordYMessage(y));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
24
lib/api/middlewares/cors.js
Normal file
24
lib/api/middlewares/cors.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function cors () {
|
||||
return function corsMiddleware (req, res, next) {
|
||||
const headers = [
|
||||
'X-Requested-With',
|
||||
'X-Prototype-Version',
|
||||
'X-CSRF-Token',
|
||||
'Authorization',
|
||||
'Carto-Event',
|
||||
'Carto-Event-Source',
|
||||
'Carto-Event-Group-Id'
|
||||
];
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
headers.push('Content-Type');
|
||||
}
|
||||
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.set('Access-Control-Allow-Headers', headers.join(', '));
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
87
lib/api/middlewares/credentials.js
Normal file
87
lib/api/middlewares/credentials.js
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
const basicAuth = require('basic-auth');
|
||||
|
||||
module.exports = function credentials () {
|
||||
return function credentialsMiddleware (req, res, next) {
|
||||
const apikeyCredentials = getApikeyCredentialsFromRequest(req);
|
||||
|
||||
res.locals.api_key = apikeyCredentials.token;
|
||||
res.locals.basicAuthUsername = apikeyCredentials.username;
|
||||
res.set('vary', 'Authorization'); // Honor Authorization header when caching.
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
function getApikeyCredentialsFromRequest (req) {
|
||||
let apikeyCredentials = {
|
||||
token: null,
|
||||
username: null
|
||||
};
|
||||
|
||||
for (const getter of apikeyGetters) {
|
||||
apikeyCredentials = getter(req);
|
||||
if (apikeyTokenFound(apikeyCredentials)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return apikeyCredentials;
|
||||
}
|
||||
|
||||
const apikeyGetters = [
|
||||
getApikeyTokenFromHeaderAuthorization,
|
||||
getApikeyTokenFromRequestQueryString,
|
||||
getApikeyTokenFromRequestBody
|
||||
];
|
||||
|
||||
function getApikeyTokenFromHeaderAuthorization (req) {
|
||||
const credentials = basicAuth(req);
|
||||
|
||||
if (credentials) {
|
||||
return {
|
||||
username: credentials.username,
|
||||
token: credentials.pass
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
username: null,
|
||||
token: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestQueryString (req) {
|
||||
let token = null;
|
||||
|
||||
if (req.query && req.query.api_key) {
|
||||
token = req.query.api_key;
|
||||
} else if (req.query && req.query.map_key) {
|
||||
token = req.query.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token
|
||||
};
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestBody (req) {
|
||||
let token = null;
|
||||
|
||||
if (req.body && req.body.api_key) {
|
||||
token = req.body.api_key;
|
||||
} else if (req.body && req.body.map_key) {
|
||||
token = req.body.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token
|
||||
};
|
||||
}
|
||||
|
||||
function apikeyTokenFound (apikey) {
|
||||
return !!apikey && !!apikey.token;
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function dbConnSetupMiddleware(pgConnection) {
|
||||
return function dbConnSetup(req, res, next) {
|
||||
const user = res.locals.user;
|
||||
module.exports = function dbConnSetup (pgConnection) {
|
||||
return function dbConnSetupMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
pgConnection.setDBConn(user, res.locals, (err) => {
|
||||
if (err) {
|
||||
if (err.message && -1 !== err.message.indexOf('name not found')) {
|
||||
if (err.message && err.message.indexOf('name not found') !== -1) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
req.profiler.done('req2params');
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(res.locals, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
|
||||
res.set('X-Served-By-DB-Host', res.locals.dbhost);
|
||||
|
||||
req.profiler.done('req2params');
|
||||
|
||||
next(null);
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
172
lib/api/middlewares/error-middleware.js
Normal file
172
lib/api/middlewares/error-middleware.js
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
const setCommonHeaders = require('../../utils/common-headers');
|
||||
|
||||
module.exports = function errorMiddleware (/* options */) {
|
||||
return function error (err, req, res, next) {
|
||||
const { logger } = res.locals;
|
||||
const errors = populateLimitErrors(Array.isArray(err) ? err : [err]);
|
||||
|
||||
errors.forEach((err) => logger.error({ exception: err }, 'Error while handling the request'));
|
||||
|
||||
setCommonHeaders(req, res, () => {
|
||||
const errorResponseBody = {
|
||||
errors: errors.map(errorMessage),
|
||||
errors_with_context: errors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
res.status(req.query.callback ? 200 : findStatusCode(errors[0]));
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(errorResponseBody);
|
||||
} else {
|
||||
res.json(errorResponseBody);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (errorTypes) {
|
||||
return errorTypes.renderTimeoutError || errorTypes.datasourceTimeoutError;
|
||||
}
|
||||
|
||||
function getErrorTypes (error) {
|
||||
return {
|
||||
renderTimeoutError: isRenderTimeoutError(error),
|
||||
datasourceTimeoutError: isDatasourceTimeoutError(error)
|
||||
};
|
||||
}
|
||||
|
||||
function isMaxWaitingClientsError (err) {
|
||||
return err.message === 'max waitingClients count exceeded';
|
||||
}
|
||||
|
||||
function populateLimitErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isMaxWaitingClientsError(error)) {
|
||||
error.message = 'You are over platform\'s limits: Max render capacity exceeded.' +
|
||||
' Contact CARTO support for more details.';
|
||||
error.type = 'limit';
|
||||
error.subtype = 'render-capacity';
|
||||
error.http_status = 429;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const errorTypes = getErrorTypes(error);
|
||||
|
||||
if (isTimeoutError(errorTypes)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
if (errorTypes.datasourceTimeoutError) {
|
||||
error.subtype = 'datasource';
|
||||
error.message = 'You are over platform\'s limits: SQL query timeout error.' +
|
||||
' Refactor your query before running again or contact CARTO support for more details.';
|
||||
}
|
||||
|
||||
if (errorTypes.renderTimeoutError) {
|
||||
error.subtype = 'render';
|
||||
error.message = 'You are over platform\'s limits: Render timeout error.' +
|
||||
' Contact CARTO support for more details.';
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
var statusCode = 400;
|
||||
if (errMsg.indexOf('permission denied') !== -1) {
|
||||
statusCode = 403;
|
||||
} else if (errMsg.indexOf('authentication failed') !== -1) {
|
||||
statusCode = 403;
|
||||
} else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
} else if (errMsg.indexOf('does not exist') !== -1) {
|
||||
if (errMsg.indexOf(' role ') !== -1) {
|
||||
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;
|
||||
}
|
||||
|
||||
function errorMessage (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (typeof err === 'string' ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessageWithContext (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (typeof err === 'string' ? 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 (Object.prototype.hasOwnProperty.call(err, prop) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
16
lib/api/middlewares/increment-map-view-count.js
Normal file
16
lib/api/middlewares/increment-map-view-count.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function incrementMapViewCount (metadataBackend) {
|
||||
return function incrementMapViewCountMiddleware (req, res, next) {
|
||||
const { mapConfig, user, logger } = res.locals;
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
11
lib/api/middlewares/initialize-status-code.js
Normal file
11
lib/api/middlewares/initialize-status-code.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function initializeStatusCode () {
|
||||
return function initializeStatusCodeMiddleware (req, res, next) {
|
||||
if (req.method !== 'OPTIONS') {
|
||||
res.statusCode = 404;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
44
lib/api/middlewares/last-modified-header.js
Normal file
44
lib/api/middlewares/last-modified-header.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setLastModifiedHeader () {
|
||||
return function setLastModifiedHeaderMiddleware (req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { mapConfigProvider, cache_buster: cacheBuster, logger } = res.locals;
|
||||
|
||||
if (cacheBuster) {
|
||||
const cacheBusterTimestamp = parseInt(cacheBuster, 10);
|
||||
const lastModifiedDate = Number.isFinite(cacheBusterTimestamp) && cacheBusterTimestamp !== 0
|
||||
? new Date(cacheBusterTimestamp)
|
||||
: new Date();
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Error generating Last Modified Header');
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
res.set('Last-Modified', new Date().toUTCString());
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const lastUpdatedAt = affectedTables.getLastUpdatedAt();
|
||||
const lastModifiedDate = Number.isFinite(lastUpdatedAt) ? new Date(lastUpdatedAt) : new Date();
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
res.locals.cache_buster = lastUpdatedAt;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
47
lib/api/middlewares/last-updated-time-layergroup.js
Normal file
47
lib/api/middlewares/last-updated-time-layergroup.js
Normal file
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setLastUpdatedTimeToLayergroup () {
|
||||
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
|
||||
const { mapConfigProvider, analysesResults } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
mapConfigProvider.createAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
res.locals.cache_buster = 0;
|
||||
layergroup.layergroupid = `${layergroup.layergroupid}:${res.locals.cache_buster}`;
|
||||
layergroup.last_updated = new Date(res.locals.cache_buster).toISOString();
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
var lastUpdateTime = affectedTables.getLastUpdatedAt();
|
||||
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
res.locals.cache_buster = lastUpdateTime;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
28
lib/api/middlewares/layer-stats.js
Normal file
28
lib/api/middlewares/layer-stats.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setLayerStats (pgConnection, statsBackend) {
|
||||
return function setLayerStatsMiddleware (req, res, next) {
|
||||
const { user, mapConfig } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
statsBackend.getStats(mapConfig, connection, function (err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
18
lib/api/middlewares/layergroup-id-header.js
Normal file
18
lib/api/middlewares/layergroup-id-header.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setLayergroupIdHeader (templateMaps, useTemplateHash) {
|
||||
return function setLayergroupIdHeaderMiddleware (req, res, next) {
|
||||
const { user, template } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
if (useTemplateHash) {
|
||||
const templateHash = templateMaps.fingerPrint(template).substring(0, 8);
|
||||
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
|
||||
res.locals.templateHash = templateHash;
|
||||
}
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
17
lib/api/middlewares/layergroup-metadata.js
Normal file
17
lib/api/middlewares/layergroup-metadata.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQuery) {
|
||||
return function setMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapConfig, analysesResults = [], context, api_key: userApiKey } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapConfig.obj());
|
||||
layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addDateWrappingMetadata(layergroup, mapConfig.obj());
|
||||
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig, userApiKey);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
34
lib/api/middlewares/layergroup-token.js
Normal file
34
lib/api/middlewares/layergroup-token.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const LayergroupToken = require('../../models/layergroup-token');
|
||||
const authErrorMessageTemplate = function (signer, user) {
|
||||
return `Cannot use map signature of user "${signer}" on db of user "${user}"`;
|
||||
};
|
||||
|
||||
module.exports = function layergroupToken () {
|
||||
return function layergroupTokenMiddleware (req, res, next) {
|
||||
const user = res.locals.user;
|
||||
const layergroupToken = LayergroupToken.parse(req.params.token);
|
||||
|
||||
res.locals.token = layergroupToken.token;
|
||||
res.locals.cache_buster = layergroupToken.cacheBuster;
|
||||
|
||||
if (layergroupToken.templateHash) {
|
||||
res.locals.templateHash = layergroupToken.templateHash;
|
||||
}
|
||||
|
||||
if (layergroupToken.signer) {
|
||||
res.locals.signer = layergroupToken.signer;
|
||||
|
||||
if (res.locals.signer !== user) {
|
||||
const err = new Error(authErrorMessageTemplate(res.locals.signer, user));
|
||||
err.type = 'auth';
|
||||
err.http_status = (req.query && req.query.callback) ? 200 : 403;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
12
lib/api/middlewares/logger.js
Normal file
12
lib/api/middlewares/logger.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const uuid = require('uuid');
|
||||
|
||||
module.exports = function initLogger ({ logger }) {
|
||||
return function initLoggerMiddleware (req, res, next) {
|
||||
res.locals.logger = logger.child({ request_id: req.get('X-Request-Id') || uuid.v4(), 'cdb-user': res.locals.user });
|
||||
res.locals.logger.info({ client_request: req }, 'Incoming request');
|
||||
res.on('finish', () => res.locals.logger.info({ server_response: res, status: res.statusCode }, 'Response sent'));
|
||||
next();
|
||||
};
|
||||
};
|
||||
33
lib/api/middlewares/lzma.js
Normal file
33
lib/api/middlewares/lzma.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const LZMA = require('lzma').LZMA;
|
||||
|
||||
module.exports = function lzma () {
|
||||
const lzmaWorker = new LZMA();
|
||||
|
||||
return function lzmaMiddleware (req, res, next) {
|
||||
if (!Object.prototype.hasOwnProperty.call(req.query, 'lzma')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = Buffer.from(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function (result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
36
lib/api/middlewares/map-error.js
Normal file
36
lib/api/middlewares/map-error.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function mapError (options) {
|
||||
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
|
||||
|
||||
return function mapErrorMiddleware (err, req, res, next) {
|
||||
const { mapConfig } = res.locals;
|
||||
|
||||
if (addContext) {
|
||||
err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err;
|
||||
}
|
||||
|
||||
err.label = label;
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
function populateError (err, mapConfig) {
|
||||
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)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
52
lib/api/middlewares/map-store-map-config-provider.js
Normal file
52
lib/api/middlewares/map-store-map-config-provider.js
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const MapStoreMapConfigProvider = require('../../models/mapconfig/provider/map-store-provider');
|
||||
|
||||
module.exports = function createMapStoreMapConfigProvider (
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
forcedFormat = null
|
||||
) {
|
||||
return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
|
||||
const { user, token, cache_buster: cacheBuster, api_key: apiKey } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { layer: layerFromParams, z, x, y, scale_factor: scaleFactor, format } = req.params;
|
||||
const { layer: layerFromQuery } = req.query;
|
||||
|
||||
const params = {
|
||||
user,
|
||||
token,
|
||||
cache_buster: cacheBuster,
|
||||
api_key: apiKey,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport,
|
||||
layer: (layerFromQuery || layerFromParams),
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
scale_factor: scaleFactor,
|
||||
format
|
||||
};
|
||||
|
||||
if (forcedFormat) {
|
||||
params.format = forcedFormat;
|
||||
params.layer = params.layer || 'all';
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
mapStore,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
params
|
||||
);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
181
lib/api/middlewares/metrics.js
Normal file
181
lib/api/middlewares/metrics.js
Normal file
@@ -0,0 +1,181 @@
|
||||
'use strict';
|
||||
|
||||
const EVENT_VERSION = '1';
|
||||
const MAX_LENGTH = 100;
|
||||
|
||||
module.exports = function metrics ({ enabled, tags, metricsBackend }) {
|
||||
if (!enabled) {
|
||||
return function metricsDisabledMiddleware (req, res, next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
if (!tags || !tags.event) {
|
||||
throw new Error('Missing required "event" parameter to report metrics');
|
||||
}
|
||||
|
||||
return function metricsMiddleware (req, res, next) {
|
||||
// FIXME: use parent logger as we don't want bind the error to the request
|
||||
// but we still want to know if an error is thrown
|
||||
const { logger } = res.locals;
|
||||
|
||||
res.on('finish', () => {
|
||||
const { event, attributes } = getEventData(req, res, tags);
|
||||
|
||||
metricsBackend.send(event, attributes)
|
||||
.catch((err) => logger.error({ exception: err, event }, 'Failed to publish event'));
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
function getEventData (req, res, tags) {
|
||||
const event = tags.event;
|
||||
const extra = {};
|
||||
if (tags.from) {
|
||||
if (tags.from.req) {
|
||||
Object.assign(extra, getFromReq(req, tags.from.req));
|
||||
}
|
||||
|
||||
if (tags.from.res) {
|
||||
Object.assign(extra, getFromRes(res, tags.from.res));
|
||||
}
|
||||
}
|
||||
|
||||
const attributes = Object.assign({}, {
|
||||
client_event: normalizedField(req.get('Carto-Event')),
|
||||
client_event_group_id: normalizedField(req.get('Carto-Event-Group-Id')),
|
||||
event_source: normalizedField(req.get('Carto-Event-Source')),
|
||||
event_time: new Date().toISOString(),
|
||||
user_id: res.locals.userId,
|
||||
user_agent: req.get('User-Agent'),
|
||||
map_id: getLayergroupid({ res }),
|
||||
cache_buster: getCacheBuster({ res }),
|
||||
template_hash: getTemplateHash({ res }),
|
||||
stat_tag: getStatTag({ res }),
|
||||
response_code: res.statusCode.toString(),
|
||||
response_time: getResponseTime(req),
|
||||
source_domain: req.hostname,
|
||||
event_version: EVENT_VERSION
|
||||
}, tags.attributes, extra);
|
||||
|
||||
// remove undefined properties
|
||||
Object.keys(attributes).forEach(key => attributes[key] === undefined && delete attributes[key]);
|
||||
|
||||
return { event, attributes };
|
||||
}
|
||||
|
||||
function normalizedField (field) {
|
||||
if (!field) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return field.toString().trim().substr(0, MAX_LENGTH);
|
||||
}
|
||||
|
||||
function getLayergroupid ({ res }) {
|
||||
if (res.locals.token) {
|
||||
return res.locals.token;
|
||||
}
|
||||
|
||||
if (res.locals.mapConfig) {
|
||||
return res.locals.mapConfig.id();
|
||||
}
|
||||
|
||||
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.mapConfig) {
|
||||
return res.locals.mapConfigProvider.mapConfig.id();
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheBuster ({ res }) {
|
||||
if (res.locals.cache_buster !== undefined) {
|
||||
return `${res.locals.cache_buster}`;
|
||||
}
|
||||
|
||||
if (res.locals.mapConfigProvider) {
|
||||
return `${res.locals.mapConfigProvider.getCacheBuster()}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplateHash ({ res }) {
|
||||
if (res.locals.templateHash) {
|
||||
return res.locals.templateHash;
|
||||
}
|
||||
|
||||
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.getTemplateHash) {
|
||||
let templateHash;
|
||||
|
||||
try {
|
||||
templateHash = res.locals.mapConfigProvider.getTemplateHash().substring(0, 8);
|
||||
} catch (e) {}
|
||||
|
||||
return templateHash;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatTag ({ res }) {
|
||||
if (res.locals.mapConfig) {
|
||||
return res.locals.mapConfig.obj().stat_tag;
|
||||
}
|
||||
|
||||
// FIXME: don't expect that mapConfig is already set
|
||||
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.mapConfig) {
|
||||
return res.locals.mapConfigProvider.mapConfig.obj().stat_tag;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: 'Profiler' might not be accurate enough
|
||||
function getResponseTime (req) {
|
||||
let stats;
|
||||
|
||||
try {
|
||||
stats = req.profiler.toJSON();
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return stats && stats.total ? stats.total.toString() : undefined;
|
||||
}
|
||||
|
||||
function getFromReq (req, { query = {}, body = {}, params = {}, headers = {} } = {}) {
|
||||
const extra = {};
|
||||
|
||||
for (const [queryParam, eventName] of Object.entries(query)) {
|
||||
extra[eventName] = req.query[queryParam];
|
||||
}
|
||||
|
||||
for (const [bodyParam, eventName] of Object.entries(body)) {
|
||||
extra[eventName] = req.body[bodyParam];
|
||||
}
|
||||
|
||||
for (const [pathParam, eventName] of Object.entries(params)) {
|
||||
extra[eventName] = req.params[pathParam];
|
||||
}
|
||||
|
||||
for (const [header, eventName] of Object.entries(headers)) {
|
||||
extra[eventName] = req.get(header);
|
||||
}
|
||||
|
||||
return extra;
|
||||
}
|
||||
|
||||
function getFromRes (res, { body = {}, headers = {}, locals = {} } = {}) {
|
||||
const extra = {};
|
||||
|
||||
if (res.body) {
|
||||
for (const [bodyParam, eventName] of Object.entries(body)) {
|
||||
extra[eventName] = res.body[bodyParam];
|
||||
}
|
||||
}
|
||||
|
||||
for (const [header, eventName] of Object.entries(headers)) {
|
||||
extra[eventName] = res.get(header);
|
||||
}
|
||||
|
||||
for (const [localParam, eventName] of Object.entries(locals)) {
|
||||
extra[eventName] = res.locals[localParam];
|
||||
}
|
||||
|
||||
return extra;
|
||||
}
|
||||
46
lib/api/middlewares/named-map-provider.js
Normal file
46
lib/api/middlewares/named-map-provider.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = null }) {
|
||||
return function getNamedMapProviderMiddleware (req, res, next) {
|
||||
const { user, token, cache_buster: cacheBuster, api_key: apiKey } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id: templateId, layer: layerFromParams, z, x, y, format } = req.params;
|
||||
const { layer: layerFromQuery } = req.query;
|
||||
|
||||
const params = {
|
||||
user,
|
||||
token,
|
||||
cache_buster: cacheBuster,
|
||||
api_key: apiKey,
|
||||
dbuser,
|
||||
dbname,
|
||||
dbpassword,
|
||||
dbhost,
|
||||
dbport,
|
||||
template_id: templateId,
|
||||
layer: (layerFromQuery || layerFromParams),
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
format
|
||||
};
|
||||
|
||||
if (forcedFormat) {
|
||||
params.format = forcedFormat;
|
||||
params.layer = params.layer || 'all';
|
||||
}
|
||||
|
||||
const { config, auth_token: authToken } = req.query;
|
||||
|
||||
namedMapProviderCache.get(user, templateId, config, authToken, params, (err, namedMapProvider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = namedMapProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
7
lib/api/middlewares/noop.js
Normal file
7
lib/api/middlewares/noop.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
};
|
||||
};
|
||||
37
lib/api/middlewares/profiler.js
Normal file
37
lib/api/middlewares/profiler.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const Profiler = require('../../stats/profiler-proxy');
|
||||
const debug = require('debug')('windshaft:cartodb:stats');
|
||||
const { name: prefix } = require('../../../package.json');
|
||||
|
||||
module.exports = function profiler (options) {
|
||||
const { enabled = true, statsClient } = options;
|
||||
|
||||
return function profilerMiddleware (req, res, next) {
|
||||
const { logger } = res.locals;
|
||||
|
||||
// TODO: stop using profiler and log stats instead of adding them to the profiler
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: statsClient,
|
||||
profile: enabled
|
||||
});
|
||||
|
||||
req.profiler.start(prefix);
|
||||
|
||||
res.on('finish', () => {
|
||||
req.profiler.done('response');
|
||||
req.profiler.end();
|
||||
const stats = req.profiler.toJSON();
|
||||
logger.info({ stats, duration: stats.response / 1000, duration_ms: stats.response }, 'Request profiling stats');
|
||||
|
||||
try {
|
||||
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug('error sending profiling stats: ' + err);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
71
lib/api/middlewares/rate-limit.js
Normal file
71
lib/api/middlewares/rate-limit.js
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const RATE_LIMIT_ENDPOINTS_GROUPS = {
|
||||
ANONYMOUS: 'anonymous',
|
||||
STATIC: 'static',
|
||||
STATIC_NAMED: 'static_named',
|
||||
DATAVIEW: 'dataview',
|
||||
DATAVIEW_SEARCH: 'dataview_search',
|
||||
ANALYSIS: 'analysis',
|
||||
ANALYSIS_CATALOG: 'analysis_catalog',
|
||||
TILE: 'tile',
|
||||
ATTRIBUTES: 'attributes',
|
||||
NAMED_LIST: 'named_list',
|
||||
NAMED_CREATE: 'named_create',
|
||||
NAMED_GET: 'named_get',
|
||||
NAMED: 'named',
|
||||
NAMED_UPDATE: 'named_update',
|
||||
NAMED_DELETE: 'named_delete',
|
||||
NAMED_TILES: 'named_tiles'
|
||||
};
|
||||
|
||||
function rateLimit (userLimitsBackend, endpointGroup = null) {
|
||||
if (!isRateLimitEnabled(endpointGroup)) {
|
||||
return function rateLimitDisabledMiddleware (req, res, next) { next(); };
|
||||
}
|
||||
|
||||
return function rateLimitMiddleware (req, res, next) {
|
||||
userLimitsBackend.getRateLimit(res.locals.user, endpointGroup, function (err, userRateLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!userRateLimit) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
|
||||
|
||||
res.set({
|
||||
'Carto-Rate-Limit-Limit': limit,
|
||||
'Carto-Rate-Limit-Remaining': remaining,
|
||||
'Carto-Rate-Limit-Reset': reset
|
||||
});
|
||||
|
||||
if (isBlocked) {
|
||||
// retry is floor rounded in seconds by redis-cell
|
||||
res.set('Retry-After', retry + 1);
|
||||
|
||||
const rateLimitError = new Error(
|
||||
'You are over platform\'s limits: too many requests.' +
|
||||
' Please contact us to know more details'
|
||||
);
|
||||
rateLimitError.http_status = 429;
|
||||
rateLimitError.type = 'limit';
|
||||
rateLimitError.subtype = 'rate-limit';
|
||||
return next(rateLimitError);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function isRateLimitEnabled (endpointGroup) {
|
||||
return global.environment.enabledFeatures.rateLimitsEnabled &&
|
||||
endpointGroup &&
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup];
|
||||
}
|
||||
|
||||
module.exports = rateLimit;
|
||||
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;
|
||||
24
lib/api/middlewares/send-response.js
Normal file
24
lib/api/middlewares/send-response.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const setCommonHeaders = require('../../utils/common-headers');
|
||||
|
||||
module.exports = function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res, next) {
|
||||
setCommonHeaders(req, res, () => {
|
||||
res.status(res.statusCode);
|
||||
|
||||
if (Buffer.isBuffer(res.body)) {
|
||||
res.send(res.body);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.query.callback) {
|
||||
res.jsonp(res.body);
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json(res.body);
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
13
lib/api/middlewares/served-by-host-header.js
Normal file
13
lib/api/middlewares/served-by-host-header.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
|
||||
module.exports = function servedByHostHeader () {
|
||||
const hostname = os.hostname().split('.')[0];
|
||||
|
||||
return function servedByHostHeaderMiddleware (req, res, next) {
|
||||
res.set('X-Served-By-Host', hostname);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
33
lib/api/middlewares/surrogate-key-header.js
Normal file
33
lib/api/middlewares/surrogate-key-header.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const NamedMapsCacheEntry = require('../../cache/model/named-maps-entry');
|
||||
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
|
||||
|
||||
module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) {
|
||||
return function setSurrogateKeyHeaderMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider, logger } = res.locals;
|
||||
|
||||
if (mapConfigProvider instanceof NamedMapMapConfigProvider) {
|
||||
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, mapConfigProvider.getTemplateName()));
|
||||
}
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
logger.warn({ exception: err }, 'Error generating Surrogate Key Header');
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables || !affectedTables.tables || affectedTables.tables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
surrogateKeysCache.tag(res, affectedTables);
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
12
lib/api/middlewares/syntax-error.js
Normal file
12
lib/api/middlewares/syntax-error.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function syntaxError () {
|
||||
return function syntaxErrorMiddleware (err, req, res, next) {
|
||||
if (err.name === 'SyntaxError') {
|
||||
err.http_status = 400;
|
||||
err.message = `${err.name}: ${err.message}`;
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
15
lib/api/middlewares/tag.js
Normal file
15
lib/api/middlewares/tag.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function tag ({ tags }) {
|
||||
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === 'string')) {
|
||||
throw new Error('Required "tags" option must be a valid Array: [string, string, ...]');
|
||||
}
|
||||
|
||||
return function tagMiddleware (req, res, next) {
|
||||
const { logger } = res.locals;
|
||||
res.locals.tags = tags;
|
||||
res.on('finish', () => logger.info({ tags: res.locals.tags }, 'Request tagged'));
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
29
lib/api/middlewares/user.js
Normal file
29
lib/api/middlewares/user.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const CdbRequest = require('../../models/cdb-request');
|
||||
|
||||
module.exports = function user (metadataBackend) {
|
||||
const cdbRequest = new CdbRequest();
|
||||
|
||||
return function userMiddleware (req, res, next) {
|
||||
try {
|
||||
res.locals.user = getUserNameFromRequest(req, cdbRequest);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
metadataBackend.getUserId(res.locals.user, (err, userId) => {
|
||||
if (err || !userId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.userId = userId;
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function getUserNameFromRequest (req, cdbRequest) {
|
||||
return cdbRequest.userByReq(req);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const timeoutErrorVectorTile = fs.readFileSync(path.join(__dirname, '/../../../assets/render-timeout-fallback.mvt'));
|
||||
|
||||
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
|
||||
|
||||
module.exports = function vectorError() {
|
||||
return function vectorErrorMiddleware(err, req, res, next) {
|
||||
if(req.params.format === 'mvt') {
|
||||
|
||||
if (isTimeoutError(err)) {
|
||||
module.exports = function vectorError () {
|
||||
return function vectorErrorMiddleware (err, req, res, next) {
|
||||
if (req.params.format === 'mvt') {
|
||||
if (isTimeoutError(err) || isRateLimitError(err)) {
|
||||
res.set('Content-Type', 'application/x-protobuf');
|
||||
return res.status(429).send(timeoutErrorVectorTile);
|
||||
}
|
||||
@@ -16,7 +17,6 @@ module.exports = function vectorError() {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
@@ -28,3 +28,7 @@ function isDatasourceTimeoutError (err) {
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function isRateLimitError (err) {
|
||||
return err.type === 'limit' && err.subtype === 'rate-limit';
|
||||
}
|
||||
229
lib/api/template/admin-template-controller.js
Normal file
229
lib/api/template/admin-template-controller.js
Normal file
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
const { templateName } = require('../../backends/template-maps');
|
||||
const tag = require('../middlewares/tag');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class AdminTemplateController {
|
||||
/**
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
constructor (authBackend, templateMaps, userLimitsBackend) {
|
||||
this.authBackend = authBackend;
|
||||
this.templateMaps = templateMaps;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
route (templateRouter) {
|
||||
templateRouter.options('/:template_id');
|
||||
|
||||
templateRouter.post('/', this.middlewares({
|
||||
action: 'create',
|
||||
label: 'POST TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_CREATE
|
||||
}));
|
||||
|
||||
templateRouter.put('/:template_id', this.middlewares({
|
||||
action: 'update',
|
||||
label: 'PUT TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_UPDATE
|
||||
}));
|
||||
|
||||
templateRouter.get('/:template_id', this.middlewares({
|
||||
action: 'get',
|
||||
label: 'GET TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_GET
|
||||
}));
|
||||
|
||||
templateRouter.delete('/:template_id', this.middlewares({
|
||||
action: 'delete',
|
||||
label: 'DELETE TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_DELETE
|
||||
}));
|
||||
|
||||
templateRouter.get('/', this.middlewares({
|
||||
action: 'list',
|
||||
label: 'GET TEMPLATE LIST',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_LIST
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ action, label, rateLimitGroup }) {
|
||||
let template;
|
||||
|
||||
if (action === 'create') {
|
||||
template = createTemplate;
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
template = updateTemplate;
|
||||
}
|
||||
|
||||
if (action === 'get') {
|
||||
template = retrieveTemplate;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
template = destroyTemplate;
|
||||
}
|
||||
|
||||
if (action === 'list') {
|
||||
template = listTemplates;
|
||||
}
|
||||
|
||||
return [
|
||||
tag({ tags: ['named', 'admin', action] }),
|
||||
credentials(),
|
||||
authorizedByAPIKey({ authBackend: this.authBackend, action, label }),
|
||||
rateLimit(this.userLimitsBackend, rateLimitGroup),
|
||||
checkContentType({ action: 'POST', label: 'POST TEMPLATE' }),
|
||||
template({ templateMaps: this.templateMaps })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkContentType ({ label }) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if ((req.method === 'POST' || req.method === 'PUT') && !req.is('application/json')) {
|
||||
const error = new Error(`${req.method} template data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function authorizedByAPIKey ({ authBackend, action, label }) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
authBackend.authorizedByAPIKey(user, res, (err, authenticated, apikey) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated users can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
if (apikey.type !== 'master') {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-does-not-grant-access';
|
||||
error.http_status = 403;
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createTemplate ({ templateMaps }) {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateTemplate ({ templateMaps }) {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveTemplate ({ templateMaps }) {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete template.auth_id;
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function destroyTemplate ({ templateMaps }) {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 204;
|
||||
res.body = '';
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function listTemplates ({ templateMaps }) {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_ids: templateIds };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
235
lib/api/template/named-template-controller.js
Normal file
235
lib/api/template/named-template-controller.js
Normal file
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const checkJsonContentType = require('../middlewares/check-json-content-type');
|
||||
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
|
||||
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
|
||||
const layerStats = require('../middlewares/layer-stats');
|
||||
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
|
||||
const layergroupMetadata = require('../middlewares/layergroup-metadata');
|
||||
const mapError = require('../middlewares/map-error');
|
||||
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
|
||||
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const metrics = require('../middlewares/metrics');
|
||||
|
||||
module.exports = class NamedMapController {
|
||||
/**
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsBackend} userLimitsBackend
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param layergroupMetadata
|
||||
* @constructor
|
||||
*/
|
||||
constructor (
|
||||
config,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTables,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
metricsBackend
|
||||
) {
|
||||
this.config = config;
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.statsBackend = statsBackend;
|
||||
this.authBackend = authBackend;
|
||||
this.layergroupMetadata = layergroupMetadata;
|
||||
this.metricsBackend = metricsBackend;
|
||||
}
|
||||
|
||||
route (templateRouter) {
|
||||
templateRouter.get('/:template_id/jsonp', this.middlewares());
|
||||
templateRouter.post('/:template_id', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
const useTemplateHash = true;
|
||||
const includeQuery = false;
|
||||
const label = 'NAMED MAP LAYERGROUP';
|
||||
const addContext = false;
|
||||
const metricsTags = {
|
||||
event: 'map_view',
|
||||
attributes: { map_type: 'named' },
|
||||
from: {
|
||||
req: {
|
||||
query: { client: 'client' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
tag({ tags: ['map', 'named'] }),
|
||||
metrics({
|
||||
enabled: this.config.pubSubMetrics.enabled,
|
||||
metricsBackend: this.metricsBackend,
|
||||
tags: metricsTags
|
||||
}),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED),
|
||||
cleanUpQueryParams(['aggregation']),
|
||||
checkJsonContentType(),
|
||||
checkInstantiteLayergroup(),
|
||||
getTemplate(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsBackend,
|
||||
this.mapConfigAdapter,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
instantiateLayergroup(
|
||||
this.mapBackend,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
incrementMapViewCount(this.metadataBackend),
|
||||
augmentLayergroupData(),
|
||||
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
lastUpdatedTimeLayergroup(),
|
||||
layerStats(this.pgConnection, this.statsBackend),
|
||||
layergroupIdHeader(this.templateMaps, useTemplateHash),
|
||||
layergroupMetadata(this.layergroupMetadata, includeQuery),
|
||||
mapError({ label, addContext })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkInstantiteLayergroup () {
|
||||
return function checkInstantiteLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
|
||||
if (callback === undefined || callback.length === 0) {
|
||||
return next(new Error('callback parameter should be present and be a function name'));
|
||||
}
|
||||
|
||||
if (config) {
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (e) {
|
||||
return next(new Error('Invalid config parameter, should be a valid JSON'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
function getTemplate (
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
affectedTablesCache
|
||||
) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const templateParams = req.body;
|
||||
const { user, dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id: templateId } = req.params;
|
||||
const { auth_token: authToken } = req.query;
|
||||
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
const mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
affectedTablesCache,
|
||||
user,
|
||||
templateId,
|
||||
templateParams,
|
||||
authToken,
|
||||
params
|
||||
);
|
||||
|
||||
mapConfigProvider.logger = res.locals.logger;
|
||||
|
||||
mapConfigProvider.getMapConfig((err, mapConfig, rendererParams, context, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfig = mapConfig;
|
||||
res.locals.rendererParams = rendererParams;
|
||||
res.locals.mapConfigProvider = mapConfigProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function instantiateLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
|
||||
return function instantiateLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapConfig, rendererParams } = res.locals;
|
||||
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
|
||||
mapConfig,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
rendererParams
|
||||
);
|
||||
|
||||
mapBackend.createLayergroup(mapConfig, rendererParams, mapConfigProvider, (err, layergroup, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = layergroup;
|
||||
|
||||
const { mapConfigProvider } = res.locals;
|
||||
|
||||
res.locals.analysesResults = mapConfigProvider.analysesResults;
|
||||
res.locals.template = mapConfigProvider.template;
|
||||
res.locals.context = mapConfigProvider.context;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
76
lib/api/template/template-router.js
Normal file
76
lib/api/template/template-router.js
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const NamedMapController = require('./named-template-controller');
|
||||
const AdminTemplateController = require('./admin-template-controller');
|
||||
const TileTemplateController = require('./tile-template-controller');
|
||||
|
||||
module.exports = class TemplateRouter {
|
||||
constructor ({ collaborators }) {
|
||||
const {
|
||||
config,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
metricsBackend
|
||||
} = collaborators;
|
||||
|
||||
this.namedMapController = new NamedMapController(
|
||||
config,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
metricsBackend
|
||||
);
|
||||
|
||||
this.tileTemplateController = new TileTemplateController(
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
surrogateKeysCache,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
);
|
||||
|
||||
this.adminTemplateController = new AdminTemplateController(
|
||||
authBackend,
|
||||
templateMaps,
|
||||
userLimitsBackend
|
||||
);
|
||||
}
|
||||
|
||||
route (apiRouter, routes) {
|
||||
const templateRouter = router({ mergeParams: true });
|
||||
|
||||
routes.forEach(route => {
|
||||
const { paths, middlewares = [] } = route;
|
||||
|
||||
middlewares.forEach(middleware => templateRouter.use(middleware()));
|
||||
|
||||
this.namedMapController.route(templateRouter);
|
||||
this.tileTemplateController.route(templateRouter);
|
||||
this.adminTemplateController.route(templateRouter);
|
||||
|
||||
paths.forEach(path => apiRouter.use(path, templateRouter));
|
||||
});
|
||||
}
|
||||
};
|
||||
98
lib/api/template/tile-template-controller.js
Normal file
98
lib/api/template/tile-template-controller.js
Normal file
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
|
||||
const tag = require('../middlewares/tag');
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const namedMapProvider = require('../middlewares/named-map-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const vectorError = require('../middlewares/vector-error');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class TileTemplateController {
|
||||
constructor (
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
surrogateKeysCache,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
route (templateRouter) {
|
||||
templateRouter.get('/:template_id/:layer/:z/:x/:y.(:format)', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
tag({ tags: ['tile', 'named'] }),
|
||||
coordinates(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_TILES),
|
||||
cleanUpQueryParams(),
|
||||
namedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
getTile({
|
||||
tileBackend: this.tileBackend,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
setContentTypeHeader(),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
vectorError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getTile ({ tileBackend, label }) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { layer, z, x, y, format } = req.params;
|
||||
const params = { layer, z, x, y, format };
|
||||
|
||||
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setContentTypeHeader () {
|
||||
return function setContentTypeHeaderMiddleware (req, res, next) {
|
||||
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user