Compare commits
375 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd8731b92 | ||
|
|
c15a48b870 | ||
|
|
072956addd | ||
|
|
27b5420358 | ||
|
|
7641542e67 | ||
|
|
debb174af4 | ||
|
|
2bd4c9e814 | ||
|
|
0dc7872256 | ||
|
|
1e56ba1de9 | ||
|
|
6f4e338dcb | ||
|
|
941ebf7d80 | ||
|
|
c38bf6ade8 | ||
|
|
44c4db93da | ||
|
|
f644b3a226 | ||
|
|
7c9b4b7283 | ||
|
|
8c839e214d | ||
|
|
99421b613c | ||
|
|
bc7a556297 | ||
|
|
220f1d6a73 | ||
|
|
767dde0b1e | ||
|
|
da32d96607 | ||
|
|
76da828168 | ||
|
|
068c242148 | ||
|
|
e4c409f9a5 | ||
|
|
00ffd75781 | ||
|
|
128ab53c55 | ||
|
|
ce4050e3e3 | ||
|
|
b82767c60d | ||
|
|
0fdab08600 | ||
|
|
4ba2632a92 | ||
|
|
d9e66c5964 | ||
|
|
72bebf1960 | ||
|
|
3fa2869665 | ||
|
|
e57c4c824b | ||
|
|
8e68e5395d | ||
|
|
0236935212 | ||
|
|
86e20b4b26 | ||
|
|
86d58fea7b | ||
|
|
9934d69736 | ||
|
|
ae48a01e26 | ||
|
|
4d11403be2 | ||
|
|
bcd14e4f77 | ||
|
|
60d2cc0a4f | ||
|
|
5e53920aae | ||
|
|
9c556964e5 | ||
|
|
d292a922f6 | ||
|
|
c016175a23 | ||
|
|
1b85951e06 | ||
|
|
a4e98163fb | ||
|
|
99324b15ef | ||
|
|
e34410fd2c | ||
|
|
cef7545c17 | ||
|
|
de8ed27207 | ||
|
|
0cfb204c04 | ||
|
|
fc82ca7490 | ||
|
|
183c8291bc | ||
|
|
d908ffdbca | ||
|
|
00a4f481f6 | ||
|
|
e0bd042bde | ||
|
|
f881efdc11 | ||
|
|
bda5022811 | ||
|
|
d5b5ef584d | ||
|
|
2cda43dc8d | ||
|
|
f7f513a61a | ||
|
|
940c982b68 | ||
|
|
d949d1c27f | ||
|
|
aa1d411fb8 | ||
|
|
f297374449 | ||
|
|
060b93c314 | ||
|
|
3ceeaedf02 | ||
|
|
c6ba9e6102 | ||
|
|
bf40b240d3 | ||
|
|
5d4d2bddd6 | ||
|
|
95dfd87c96 | ||
|
|
eab9e8846e | ||
|
|
788bc302a0 | ||
|
|
1ba240d099 | ||
|
|
ee0405da1e | ||
|
|
5e9b326d03 | ||
|
|
1f30367e59 | ||
|
|
26a2f73c2a | ||
|
|
60005e2f7f | ||
|
|
1c7da2c4b3 | ||
|
|
3799dd2574 | ||
|
|
7efb2a2344 | ||
|
|
88777abc2c | ||
|
|
4d9a6f8fbe | ||
|
|
3d9c2e66c5 | ||
|
|
6bbe715aa6 | ||
|
|
ba002fdb2c | ||
|
|
49c97e2cf2 | ||
|
|
41e65a9633 | ||
|
|
feae766e62 | ||
|
|
e3bdeec8ca | ||
|
|
80c4207c74 | ||
|
|
80e4306fbc | ||
|
|
543d257a20 | ||
|
|
8a023e3d2f | ||
|
|
f13b45862d | ||
|
|
731fe4c00f | ||
|
|
500cbb959f | ||
|
|
108a319143 | ||
|
|
ef5ea5b4cb | ||
|
|
10d1381e51 | ||
|
|
dfef7ff3c0 | ||
|
|
83d0ce4040 | ||
|
|
75f72c4d07 | ||
|
|
adb9e55fb2 | ||
|
|
5d3726de44 | ||
|
|
f186e4736b | ||
|
|
a00c2b1eef | ||
|
|
64d601179d | ||
|
|
cf2b73e473 | ||
|
|
70932c23df | ||
|
|
519d49bd10 | ||
|
|
bf814c4442 | ||
|
|
f136993c50 | ||
|
|
ba008ab518 | ||
|
|
e4ed6ee1cc | ||
|
|
fda7661dad | ||
|
|
79233471c6 | ||
|
|
a75beefe6e | ||
|
|
e43ccf4f12 | ||
|
|
cd8e320534 | ||
|
|
d7f4d39aa2 | ||
|
|
89333185a9 | ||
|
|
99b95cf839 | ||
|
|
9fbc56b82c | ||
|
|
9a1bc51fdb | ||
|
|
d42257127b | ||
|
|
5a730c6df1 | ||
|
|
418c8691d1 | ||
|
|
9885045b41 | ||
|
|
062e6f9594 | ||
|
|
d8428938ae | ||
|
|
ca5f280cb3 | ||
|
|
524d5a5597 | ||
|
|
a43779b050 | ||
|
|
ef3917fa6f | ||
|
|
031e1253ca | ||
|
|
8012d76b68 | ||
|
|
d726c9ad01 | ||
|
|
1ce8076699 | ||
|
|
54f32113f3 | ||
|
|
19bf079f2d | ||
|
|
b7ecde5c9d | ||
|
|
a2f804d79f | ||
|
|
efdfabf3e9 | ||
|
|
e9a4fc4b2c | ||
|
|
a1d536642e | ||
|
|
3c00266666 | ||
|
|
7f64d15944 | ||
|
|
8259271184 | ||
|
|
20366cedb4 | ||
|
|
a102d1d366 | ||
|
|
4b97b4fd26 | ||
|
|
b94debf10e | ||
|
|
60030784c1 | ||
|
|
cc9b190e5d | ||
|
|
4946ca688c | ||
|
|
d2828ecaff | ||
|
|
5a3dd6a914 | ||
|
|
bcd2fd8f88 | ||
|
|
94a5e66881 | ||
|
|
d55b78f76b | ||
|
|
42149f9ae7 | ||
|
|
1e08d946b1 | ||
|
|
f22216e6d2 | ||
|
|
d9cf830fb4 | ||
|
|
b762008c79 | ||
|
|
ca2c2b80d8 | ||
|
|
f946dfa65f | ||
|
|
418f5faa11 | ||
|
|
bba6db9dbf | ||
|
|
326cad2f2c | ||
|
|
34808d6147 | ||
|
|
79b04bbdfd | ||
|
|
45a663d5ae | ||
|
|
cace6169c0 | ||
|
|
bdce2f95f2 | ||
|
|
506e16fc87 | ||
|
|
c367743d76 | ||
|
|
fa7140e736 | ||
|
|
c63226cd26 | ||
|
|
777df6337b | ||
|
|
2dda0a80da | ||
|
|
e2bd97eea6 | ||
|
|
fb03cd3424 | ||
|
|
8a48b96c53 | ||
|
|
76b0c94835 | ||
|
|
6a36aa1f13 | ||
|
|
800870e783 | ||
|
|
6638ba91c3 | ||
|
|
47e4b9da0d | ||
|
|
81e0c3a098 | ||
|
|
2068861988 | ||
|
|
878f3bd627 | ||
|
|
170fcc1973 | ||
|
|
d0c88ce21d | ||
|
|
86d8f28661 | ||
|
|
e97147ddb4 | ||
|
|
a40bc4a527 | ||
|
|
77f64bee8c | ||
|
|
e81a16ce0d | ||
|
|
153a792fcb | ||
|
|
5c1b1e3214 | ||
|
|
0bca3d6f33 | ||
|
|
14e90a6c76 | ||
|
|
a57cd25bec | ||
|
|
a46f7b3099 | ||
|
|
cb7fb97a13 | ||
|
|
e4ae3e235d | ||
|
|
423620b6c5 | ||
|
|
877ed63090 | ||
|
|
8e9f61f9f1 | ||
|
|
81e54660bb | ||
|
|
4d6c501fa5 | ||
|
|
55d9c02f8d | ||
|
|
a0c24d132e | ||
|
|
6dd4914460 | ||
|
|
ee4e7b01a9 | ||
|
|
434de7786c | ||
|
|
07b4cb78b1 | ||
|
|
6b472c0b20 | ||
|
|
4b0a4dd675 | ||
|
|
97f8c361ed | ||
|
|
0c044636ef | ||
|
|
f95c310462 | ||
|
|
9d8ce6bc44 | ||
|
|
e4407ece84 | ||
|
|
507d105ab2 | ||
|
|
ba6cca46a1 | ||
|
|
753ada0e76 | ||
|
|
d311dccce8 | ||
|
|
b81cfe418a | ||
|
|
8ee4a2c049 | ||
|
|
a987f6ac05 | ||
|
|
b0e47ecc62 | ||
|
|
daa3fdca11 | ||
|
|
bcfc43a517 | ||
|
|
b83351a504 | ||
|
|
1edf684475 | ||
|
|
0bc68d7144 | ||
|
|
52d1cd47db | ||
|
|
98e8d745b1 | ||
|
|
e8740af6ef | ||
|
|
a00f468e62 | ||
|
|
27a52b66c6 | ||
|
|
96b9d498fd | ||
|
|
6d30903531 | ||
|
|
4a63fed943 | ||
|
|
6fe73862f3 | ||
|
|
1664975dd1 | ||
|
|
02ac25181e | ||
|
|
239aa12622 | ||
|
|
aa43eb8953 | ||
|
|
6d46a21005 | ||
|
|
fb7f79594d | ||
|
|
f390a10830 | ||
|
|
4193f96c03 | ||
|
|
afa1e2881f | ||
|
|
3fa6750f9a | ||
|
|
1c842c1592 | ||
|
|
3c88634d09 | ||
|
|
19bb11adc5 | ||
|
|
4405d61845 | ||
|
|
1bb716ef33 | ||
|
|
eb2825eea8 | ||
|
|
811f2bdae3 | ||
|
|
53bc14bc9e | ||
|
|
50ddfaa968 | ||
|
|
ae35acd21d | ||
|
|
d4d32bdfa3 | ||
|
|
3b7db0b08f | ||
|
|
43fec74372 | ||
|
|
c7f5f310f0 | ||
|
|
e26cfb2efb | ||
|
|
b32d056efe | ||
|
|
65308ea2eb | ||
|
|
8d16bf566d | ||
|
|
0b27d174ef | ||
|
|
869f2ac322 | ||
|
|
5bc1903677 | ||
|
|
faaebaa07d | ||
|
|
eceffda87f | ||
|
|
e93fe13b41 | ||
|
|
245d24ea29 | ||
|
|
605be77a04 | ||
|
|
acd0610517 | ||
|
|
e37682403c | ||
|
|
b2fcbdd8a3 | ||
|
|
2f68d658f0 | ||
|
|
85e7245a33 | ||
|
|
2db2546cca | ||
|
|
b3d7909849 | ||
|
|
b035b5d384 | ||
|
|
f52cc276be | ||
|
|
c637caf9c9 | ||
|
|
d405987a96 | ||
|
|
06efe410ef | ||
|
|
5bf4eba215 | ||
|
|
87c4848e19 | ||
|
|
3f075ca432 | ||
|
|
6725025e1a | ||
|
|
8d42909eab | ||
|
|
d947700646 | ||
|
|
cc68b84212 | ||
|
|
446449bbde | ||
|
|
b1f788fb57 | ||
|
|
f80e7112bc | ||
|
|
68f967e582 | ||
|
|
2edcbb4724 | ||
|
|
006dd86614 | ||
|
|
dab204ea71 | ||
|
|
f2fa650661 | ||
|
|
1c6c3962db | ||
|
|
aa57cdefb3 | ||
|
|
7c5b7641d8 | ||
|
|
eb4a49ec92 | ||
|
|
88f02458db | ||
|
|
1b405e42c2 | ||
|
|
bb5bfd10ee | ||
|
|
088a8b81a6 | ||
|
|
243e982bd6 | ||
|
|
dfe01c836c | ||
|
|
fcbf5ffcc5 | ||
|
|
90c9ad18e0 | ||
|
|
214d684fcc | ||
|
|
9118e2dc5e | ||
|
|
e7592ee570 | ||
|
|
81d99ca655 | ||
|
|
7b35701fa8 | ||
|
|
4f8b541010 | ||
|
|
55dd049812 | ||
|
|
66b41a6ae7 | ||
|
|
499e9de75d | ||
|
|
855f47e446 | ||
|
|
fc472e65b6 | ||
|
|
91e0e0fd18 | ||
|
|
16a36a9d7a | ||
|
|
077f19d506 | ||
|
|
ed51513b5e | ||
|
|
52630b8084 | ||
|
|
6f04214f5d | ||
|
|
f376a7cdd5 | ||
|
|
b7c6f5acdf | ||
|
|
0887e5d5f7 | ||
|
|
d01857923e | ||
|
|
deb29f2c77 | ||
|
|
d937ed31d5 | ||
|
|
73ae736603 | ||
|
|
1767b83d09 | ||
|
|
ba3af551e3 | ||
|
|
e0d4a9e596 | ||
|
|
555d3f558c | ||
|
|
386d6bfea8 | ||
|
|
479b8be639 | ||
|
|
a007fce913 | ||
|
|
100a2986b9 | ||
|
|
752bfe779e | ||
|
|
8cf878f723 | ||
|
|
605d7057c9 | ||
|
|
60e4defa66 | ||
|
|
e7b8d9b223 | ||
|
|
e041b5b8a9 | ||
|
|
4a2950796b | ||
|
|
9a8f72b8db | ||
|
|
667925c455 | ||
|
|
f24217a400 | ||
|
|
84fd01535c | ||
|
|
e362fca9eb | ||
|
|
00f81db57e | ||
|
|
0c9d60b573 | ||
|
|
ad227a5240 | ||
|
|
2b1f12e9d5 |
@@ -5,10 +5,10 @@ services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- docker pull cartoimages/windshaft-carto-testing
|
||||
- docker pull cartoimages/windshaft-testing
|
||||
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-carto-testing
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh
|
||||
|
||||
language: generic
|
||||
|
||||
|
||||
83
NEWS.md
83
NEWS.md
@@ -1,5 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
## 5.0.2
|
||||
Released 2018-07-27
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.62.1](https://github.com/CartoDB/camshaft/releases/tag/0.62.1):
|
||||
- Add support for batch street-level geocoding analysis.
|
||||
|
||||
## 5.0.1
|
||||
Released 2018-01-20
|
||||
|
||||
Bug Fixes:
|
||||
- Allow aggregation for queries with no the_geom (only the_geom_webmercator) #856
|
||||
|
||||
## 5.0.0
|
||||
Released 2018-01-29
|
||||
|
||||
Backward incompatible changes:
|
||||
- Aggregation dataview returns categories with the same type as the database type. For example, if we are aggretating by a numeric field, the resulting JSON will contain a number instead of a stringified number.
|
||||
|
||||
## 4.8.0
|
||||
Released 2018-01-04
|
||||
|
||||
New features:
|
||||
- Return url template in metadata #838.
|
||||
|
||||
Bux fixes:
|
||||
- Tests: Order torque objects before comparison
|
||||
|
||||
## 4.7.0
|
||||
Released 2018-01-03
|
||||
|
||||
New features:
|
||||
- Return tilejson in metadata #837.
|
||||
|
||||
Bug fixes:
|
||||
- Allow to create vector map-config for layers that doesn't have points. Layers with lines or polygons won't be aggregated by default.
|
||||
|
||||
|
||||
## 4.6.0
|
||||
Released 2018-01-02
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [4.2.0](https://github.com/CartoDB/windshaft/releases/tag/4.2.0).
|
||||
- Validate aggregation input params.
|
||||
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
|
||||
- Add full-sample aggregation support for vector map-config.
|
||||
|
||||
## 4.5.0
|
||||
Released 2017-12-19
|
||||
|
||||
Announcements:
|
||||
- Date histograms: Add second, decade, century and millenium aggregations
|
||||
- Date histograms: Switch the auto threshold from 366 buckets to 100.
|
||||
- Logging all errors.
|
||||
- Add support for aggregated visualizations.
|
||||
- Allow vector-only map-config creation.
|
||||
- Histograms: Now they accept a `no_filters` parameter.
|
||||
|
||||
|
||||
## 4.4.0
|
||||
Released 2017-12-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.60.0](https://github.com/CartoDB/camshaft/releases/tag/0.60.0).
|
||||
|
||||
|
||||
## 4.3.1
|
||||
Released 2017-12-12
|
||||
|
||||
Bug fix:
|
||||
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format #808
|
||||
|
||||
## 4.3.0
|
||||
Released 2017-12-11
|
||||
|
||||
Announcements:
|
||||
- Optimize Formula queries.
|
||||
- Optimize Formula queries in overviews.
|
||||
- Optimize Numeric Histogram queries.
|
||||
- Optimize Date Histogram queries.
|
||||
- Date Histograms: Now returns the same value for max/min/avg/timestamp per bin.
|
||||
- Date Histograms: Now it should return the same no matter the DB/Client time zone.
|
||||
|
||||
## 4.2.0
|
||||
Released 2017-12-04
|
||||
|
||||
|
||||
4
app.js
4
app.js
@@ -4,6 +4,7 @@ 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');
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
@@ -16,6 +17,9 @@ if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// This function should be called before the require('yargs').
|
||||
setICUEnvVariable();
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
|
||||
@@ -55,7 +55,7 @@ var config = {
|
||||
,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])'
|
||||
,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
|
||||
|
||||
@@ -55,7 +55,7 @@ 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])'
|
||||
,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
|
||||
|
||||
@@ -55,7 +55,7 @@ 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]'
|
||||
,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
|
||||
|
||||
@@ -54,7 +54,7 @@ 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])'
|
||||
,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
|
||||
|
||||
62
docs/MapConfig-Aggregation-extension.md
Normal file
62
docs/MapConfig-Aggregation-extension.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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
|
||||
|
||||
This extension introduces a new layer options for aggregated data tile generation.
|
||||
|
||||
## 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.
|
||||
|
||||
```javascript
|
||||
{
|
||||
aggregation: {
|
||||
|
||||
// OPTIONAL
|
||||
// string, defines the placement of aggregated geometries. Can be one of:
|
||||
// * "point-sample", the default places geometries at a sample point (one of the aggregated geometries)
|
||||
// * "point-grid" places geometries at the center of the aggregation grid cells
|
||||
// * "centroid" places geometriea at the average position of the aggregated points
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#placement for more details
|
||||
placement: "point-sample",
|
||||
|
||||
// OPTIONAL
|
||||
// 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: "*"}`
|
||||
// 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.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#columns for more details
|
||||
columns: {
|
||||
"aggregated_column_1": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "original_column_1"
|
||||
}
|
||||
},
|
||||
|
||||
// OPTIONAL
|
||||
// Number, defines the cell-size of the spatial aggregation grid as a pixel resolution power of two (1/4, 1/2,... 2, 4, 16)
|
||||
// to scale from 256x256 pixels; the default is 1 corresponding to 256x256 cells per tile.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#resolution for more details
|
||||
resolution: 1,
|
||||
|
||||
// OPTIONAL
|
||||
// Number, the minimum number of (estimated) rows in the dataset (query results) for aggregation to be applied.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#threshold for more details
|
||||
threshold: 500000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
187
docs/aggregation.md
Normal file
187
docs/aggregation.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# 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).
|
||||
|
||||
Aggregation is available only for point geometries. During aggregation the points are grouped using a grid; all the points laying in the same cell of the grid are summarized in a single aggregated result 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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
We might decide in the future to allow sampling column values for any of the different placement modes.
|
||||
|
||||
### 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.
|
||||
|
||||
Aggregation that would otherwise occur can be disabled by passing an `aggregation=false` parameter to the map instantiation HTTP call.
|
||||
|
||||
To control how aggregation is performed, an aggregation option can be added to the layer:
|
||||
|
||||
```json
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"sql": "SELECT * FROM data",
|
||||
"aggregation": {
|
||||
"placement": "centroid",
|
||||
"columns": {
|
||||
"value": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Even if aggregation is explicitly requested it may not be activated, e.g., if the geometries are not points
|
||||
or the whole dataset is too small. The map instantiation response contains metadata that informs if any particular
|
||||
layer will be aggregated when tiles are requested, both for vector (mvt) and raster (png) tiles.
|
||||
|
||||
```json
|
||||
{
|
||||
"layergroupid": "7b97b6e76590fef889b63edd2efb1c79:1513608333045",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"id": "layer0",
|
||||
"meta": {
|
||||
"stats": {
|
||||
"estimatedFeatureCount": 6232136
|
||||
},
|
||||
"aggregation": {
|
||||
"png": true,
|
||||
"mvt": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregation parameters
|
||||
|
||||
The aggregation parameters for a layer are defined inside an `aggregation` option of the layer:
|
||||
|
||||
```json
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"sql": "SELECT * FROM data",
|
||||
"aggregation": {"...": "..."}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `placement`
|
||||
|
||||
Determines the kind of aggregated geometry generated:
|
||||
|
||||
#### `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`
|
||||
|
||||
Generates points at the center of the aggregation grid cells (squares).
|
||||
|
||||
#### `centroid`
|
||||
|
||||
Generates points with the averaged coordinated of the grouped points (i.e. the points inside each grid cell).
|
||||
|
||||
### `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).
|
||||
The values to be aggregated are defined by the _aggregated column_ of the source data. The column keys define the name of the resulting column in the aggregated dataset.
|
||||
|
||||
For example here we define three aggregate attributes named `total`, `max_price` and `price` which are all computed with the same column, `price`,
|
||||
of the original dataset applying three different aggregate functions.
|
||||
|
||||
```json
|
||||
{
|
||||
"columns": {
|
||||
"total": { "aggregate_function": "sum", "aggregated_column": "price" },
|
||||
"max_price": { "aggregate_function": "max", "aggregated_column": "price" },
|
||||
"price": { "aggregate_function": "avg", "aggregated_column": "price" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 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`
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
0.5 would yield 4 cells per pixel. In teneral values less than 1 produce sub-pixel precision.
|
||||
|
||||
> Note that is independent of the number of pixels for raster tile or the coordinate resolution (mvt_extent) of vector tiles.
|
||||
|
||||
|
||||
### `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
|
||||
|
||||
```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": {
|
||||
"value": {
|
||||
"aggregate_function": "avg",
|
||||
"aggregated_column": "value"
|
||||
},
|
||||
"total": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
}
|
||||
},
|
||||
"resolution": 2,
|
||||
"threshold": 500000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,7 @@
|
||||
# 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)
|
||||
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
|
||||
@@ -41,6 +42,13 @@ updated_at | The ISO date of the last time the data involved in the query was up
|
||||
metadata | Includes information about the layers.
|
||||
cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
**Improved response metadata**
|
||||
|
||||
Originally, you needed to concantenate the `layergroupid` with the correct domain and the path for the tiles.
|
||||
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
|
||||
|
||||
#### Call
|
||||
@@ -61,30 +69,49 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"tilejson": {
|
||||
"raster": {
|
||||
"tilejson": "2.2.0",
|
||||
"tiles": [
|
||||
"http://a.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png",
|
||||
"http://b.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png"
|
||||
]
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raster": {
|
||||
"urlTemplate": "http://{s}.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png",
|
||||
"subdomains": ["a", "b"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
"https": "https://cdb.com",
|
||||
"templates": {
|
||||
"http": { "subdomains": ["a","b"], "url": "http://{s}.cdb.com" },
|
||||
"https": { "subdomains": ["a","b"], "url": "https://{s}.example.com" },
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
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).
|
||||
|
||||
- **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.
|
||||
- **Raster**: If a tile is requested as a raster image format, like PNG, the map will be rendered on the server, using the CartoCSS styles defined in the layers of the map. It is necessary that all the layers of a map define CartoCSS styles in order to obtain raster tiles. Raster tiles are made up of 256x256 pixels; to avoid graphic quality issues tiles should be used unscaled to represent the zoom level (Z) for which they are requested. In order to render tiles, data will be retrieved from the database (in vector format) on the server-side.
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
|
||||
|
||||
### Mapnik tiles
|
||||
### Raster tiles
|
||||
|
||||
These raster tiles retrieve just the Mapnik layers. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
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.
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
@@ -92,9 +119,9 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
|
||||
### 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.
|
||||
[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.
|
||||
|
||||
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. However, you can use any implementation tool for processing MVT files.
|
||||
CARTO uses Web Graphics Library (WebGL) to process MVT files on the browser. This is useful since WebGL is 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. However, you can use any implementation tool for processing MVT files.
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
@@ -245,7 +272,7 @@ center: [30, 0]
|
||||
map.setStyle({
|
||||
"version": 7,
|
||||
"glyphs": "...",
|
||||
"constants": {...},
|
||||
"constants": {...},
|
||||
"sources": {
|
||||
"cartodb": {
|
||||
"type": "vector",
|
||||
|
||||
@@ -37,12 +37,19 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
var noFilters = +params.no_filters;
|
||||
if (Number.isFinite(ownFilter) && Number.isFinite(noFilters)) {
|
||||
err = new Error();
|
||||
err.message = 'Both own_filter and no_filters cannot be sent in the same request';
|
||||
err.type = 'dataview';
|
||||
err.http_status = 400;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var query = getDataviewQuery(dataviewDefinition, ownFilter, noFilters);
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
@@ -55,7 +62,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, getOverrideParams(params, ownFilter), this);
|
||||
dataview.getResult(pg, getOverrideParams(params, !!ownFilter), this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result);
|
||||
@@ -63,6 +70,16 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
};
|
||||
|
||||
function getDataviewQuery(dataviewDefinition, ownFilter, noFilters) {
|
||||
if (noFilters) {
|
||||
return dataviewDefinition.sql.no_filters;
|
||||
} else if (ownFilter === 1) {
|
||||
return dataviewDefinition.sql.own_filter_on;
|
||||
} else {
|
||||
return dataviewDefinition.sql.own_filter_off;
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
|
||||
@@ -106,6 +106,7 @@ LayergroupController.prototype.register = function(app) {
|
||||
var allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'start', // number
|
||||
'end', // number
|
||||
@@ -357,7 +358,9 @@ LayergroupController.prototype.center = function(req, res, next) {
|
||||
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.format = req.params.format || 'png';
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var self = this;
|
||||
@@ -507,4 +510,4 @@ function validateLayerRouteMiddleware(req, res, next) {
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
@@ -69,6 +70,7 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['aggregation']),
|
||||
this.prepareContext,
|
||||
this.initProfiler(isTemplateInstantiation),
|
||||
this.checkJsonContentType(),
|
||||
@@ -87,6 +89,8 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
|
||||
this.setAnalysesMetadataToLayergroup(includeQuery),
|
||||
this.setTurboCartoMetadataToLayergroup(),
|
||||
this.setAggregationMetadataToLayergroup(),
|
||||
this.setTilejsonMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
@@ -310,6 +314,84 @@ MapController.prototype.augmentLayergroupData = function () {
|
||||
};
|
||||
};
|
||||
|
||||
function getTemplateUrl(url) {
|
||||
return url.https || url.http;
|
||||
}
|
||||
|
||||
function getTilejson(tiles, grids) {
|
||||
const tilejson = {
|
||||
tilejson: '2.2.0',
|
||||
tiles: tiles.https || tiles.http
|
||||
};
|
||||
|
||||
if (grids) {
|
||||
tilejson.grids = grids.https || grids.http;
|
||||
}
|
||||
|
||||
return tilejson;
|
||||
}
|
||||
|
||||
MapController.prototype.setTilejsonMetadataToLayergroup = function () {
|
||||
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
|
||||
let hasMapnikLayers = false;
|
||||
layergroup.metadata.layers.forEach((layerMetadata, index) => {
|
||||
const layerId = mapconfig.getLayerId(index);
|
||||
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
|
||||
if (mapconfig.layerType(index) === 'mapnik') {
|
||||
hasMapnikLayers = true;
|
||||
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
|
||||
const layerTilejson = {
|
||||
vector: getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
|
||||
};
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
let grids = null;
|
||||
const layer = mapconfig.getLayer(index);
|
||||
if (layer.options.interactivity) {
|
||||
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
|
||||
grids = this.resourceLocator.getTileUrls(user, gridResource);
|
||||
}
|
||||
layerTilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource),
|
||||
grids
|
||||
);
|
||||
}
|
||||
layerMetadata.tilejson = layerTilejson;
|
||||
} else {
|
||||
layerMetadata.tilejson = {
|
||||
raster: getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tilejson = {};
|
||||
const url = {};
|
||||
|
||||
if (hasMapnikLayers) {
|
||||
const vectorResource = `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`;
|
||||
tilejson.vector = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, vectorResource)
|
||||
);
|
||||
url.vector = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, vectorResource));
|
||||
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
const rasterResource = `${layergroup.layergroupid}/{z}/{x}/{y}.png`;
|
||||
tilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource)
|
||||
);
|
||||
url.raster = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, rasterResource));
|
||||
}
|
||||
}
|
||||
|
||||
layergroup.metadata.tilejson = tilejson;
|
||||
layergroup.metadata.url = url;
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
@@ -540,13 +622,13 @@ MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
|
||||
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addContextMetadata(layergroup, mapConfig, context) {
|
||||
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
@@ -557,6 +639,28 @@ function addContextMetadata(layergroup, mapConfig, context) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see how evolve this function, it's a good candidate to be refactored
|
||||
MapController.prototype.setAggregationMetadataToLayergroup = function () {
|
||||
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addAggregationContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
|
||||
layer.meta.aggregation = context.aggregation.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.setSurrogateKeyHeader = function () {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { affectedTables, user, templateName } = res.locals;
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
const vectorError = require('../middleware/vector-error');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
var vectorError = require('../middleware/vector-error');
|
||||
const DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function numMapper(n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
function getRequestParams(locals) {
|
||||
const params = Object.assign({}, locals);
|
||||
|
||||
delete params.template;
|
||||
delete params.affectedTablesAndLastUpdate;
|
||||
delete params.namedMapProvider;
|
||||
delete params.allowedQueryParams;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
@@ -27,8 +46,16 @@ NamedMapsController.prototype.register = function(app) {
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
this.getNamedMapProvider('NAMED_MAP_TILE'),
|
||||
this.getAffectedTables(),
|
||||
this.getTile('NAMED_MAP_TILE'),
|
||||
this.setSurrogateKey(),
|
||||
this.setCacheChannelHeader(),
|
||||
this.setLastModifiedHeader(),
|
||||
this.setCacheControlHeader(),
|
||||
this.setContentTypeHeader(),
|
||||
this.respond(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
@@ -37,296 +64,320 @@ NamedMapsController.prototype.register = function(app) {
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.prepareContext,
|
||||
this.staticMap.bind(this)
|
||||
this.getNamedMapProvider('STATIC_VIZ_MAP'),
|
||||
this.getAffectedTables(),
|
||||
this.getTemplate('STATIC_VIZ_MAP'),
|
||||
this.prepareLayerFilterFromPreviewLayers('STATIC_VIZ_MAP'),
|
||||
this.getStaticImageOptions(),
|
||||
this.getImage('STATIC_VIZ_MAP'),
|
||||
this.incrementMapViews(),
|
||||
this.setSurrogateKey(),
|
||||
this.setCacheChannelHeader(),
|
||||
this.setLastModifiedHeader(),
|
||||
this.setCacheControlHeader(),
|
||||
this.setContentTypeHeader(),
|
||||
this.respond()
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, body, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName()));
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
NamedMapsController.prototype.getNamedMapProvider = function (label) {
|
||||
return function getNamedMapProviderMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const { config, auth_token } = req.query;
|
||||
const { template_id } = req.params;
|
||||
|
||||
var self = this;
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
|
||||
},
|
||||
function sendResponse(err, result) {
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = namedMapProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTables (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.log('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!result || !!result.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(result.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(result.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.tile = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function getTile(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.tileBackend.getTile(namedMapProvider, req.params, this);
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'NAMED_MAP_TILE';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.staticMap = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
res.locals.format = req.params.format || 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function prepareLayerVisibility(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function incrementMapViews(err, image, headers, stats) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
|
||||
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
|
||||
if (err) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
|
||||
}
|
||||
next(err, image, headers, stats);
|
||||
});
|
||||
});
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
NamedMapsController.prototype.getTemplate = function (label) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getTemplate((err, template) => {
|
||||
if (err) {
|
||||
err.label = 'STATIC_VIZ_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.locals.template = template;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
|
||||
user,
|
||||
req,
|
||||
params,
|
||||
namedMapProvider,
|
||||
callback
|
||||
) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (label) {
|
||||
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
|
||||
const { user, template } = res.locals;
|
||||
const { template_id } = req.params;
|
||||
const { config, auth_token } = req.query;
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return callback();
|
||||
return next();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach(function (layer, index) {
|
||||
template.layergroup.layers.forEach((layer, index) => {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return callback();
|
||||
return next();
|
||||
}
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
user,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = provider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
var DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
NamedMapsController.prototype.getTile = function (label) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
this.tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = tile;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function () {
|
||||
return function getStaticImageOptionsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, template } = res.locals;
|
||||
|
||||
const imageOpts = getImageOptions(res.locals, template);
|
||||
|
||||
if (imageOpts) {
|
||||
res.locals.imageOpts = imageOpts;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = bounds;
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function getImageOptions (params, template) {
|
||||
const { zoom, lon, lat, bbox } = params;
|
||||
|
||||
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
};
|
||||
|
||||
function numMapper(n) {
|
||||
return +n;
|
||||
imageOpts = getImageOptionsFromBoundingBox(bbox);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromTemplate(template, zoom);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, params, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
|
||||
if ([params.zoom, params.lon, params.lat].map(numMapper).every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
zoom: params.zoom,
|
||||
function getImageOptionsFromCoordinates (zoom, lon, lat) {
|
||||
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
|
||||
return {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: params.lon,
|
||||
lat: params.lat
|
||||
lng: lon,
|
||||
lat: lat
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bbox = params.bbox.split(',').map(numMapper);
|
||||
if (bbox.length === 4 && bbox.every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
bounds: {
|
||||
west: bbox[0],
|
||||
south: bbox[1],
|
||||
east: bbox[2],
|
||||
north: bbox[3]
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
namedMapProvider.getTemplate(this);
|
||||
},
|
||||
function handleTemplateView(err, template) {
|
||||
assert.ifError(err);
|
||||
function getImageOptionsFromBoundingBox (bbox = '') {
|
||||
var _bbox = bbox.split(',').map(numMapper);
|
||||
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+params.zoom)) {
|
||||
zoomCenter.zoom = +params.zoom;
|
||||
}
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
|
||||
return {
|
||||
bounds: {
|
||||
west: _bbox[0],
|
||||
south: _bbox[1],
|
||||
east: _bbox[2],
|
||||
north: _bbox[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
function estimateBoundsIfNoImageOpts(err, imageOpts) {
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
NamedMapsController.prototype.getImage = function (label) {
|
||||
return function getImageMiddleware (req, res, next) {
|
||||
const { imageOpts, namedMapProvider } = res.locals;
|
||||
const { zoom, center, bounds } = imageOpts;
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
|
||||
let { width, height } = req.params;
|
||||
|
||||
width = +width;
|
||||
height = +height;
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
if (zoom !== undefined && center) {
|
||||
return this.previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
|
||||
(err, image, headers, stats) => {
|
||||
if (err) {
|
||||
return next(null);
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
|
||||
return next(null, result);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
},
|
||||
function returnCallback(err, imageOpts) {
|
||||
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
|
||||
}
|
||||
);
|
||||
|
||||
this.previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function incrementMapViewsError (ctx) {
|
||||
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.incrementMapViews = function () {
|
||||
return function incrementMapViewsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
return next();
|
||||
}
|
||||
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
this.metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
if (view.zoom !== undefined && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
@@ -337,9 +388,8 @@ function templateZoomCenter(view) {
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return Number.isFinite(view.bounds[prop]);
|
||||
});
|
||||
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
|
||||
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
@@ -355,3 +405,86 @@ function templateBounds(view) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.setCacheChannelHeader = function () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setSurrogateKey = function () {
|
||||
return function setSurrogateKeyMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
if (affectedTablesAndLastUpdate.tables.length > 0) {
|
||||
this.surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setLastModifiedHeader = function () {
|
||||
return function setLastModifiedHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setCacheControlHeader = function () {
|
||||
return function setCacheControlHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setContentTypeHeader = function () {
|
||||
return function setContentTypeHeaderMiddleware(req, res, next) {
|
||||
const { headers = {} } = res.locals;
|
||||
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.respond = function () {
|
||||
return function respondMiddleware (req, res) {
|
||||
const { body, stats = {}, format } = res.locals;
|
||||
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats);
|
||||
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
const { templateName } = require('../backends/template_maps');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -20,211 +16,189 @@ function NamedMapsAdminController(authApi, templateMaps) {
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
const { base_url_templated } = app;
|
||||
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
this.checkContentType('POST', 'POST TEMPLATE'),
|
||||
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
|
||||
this.create()
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
this.checkContentType('PUT', 'PUT TEMPLATE'),
|
||||
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
|
||||
this.update()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
|
||||
this.retrieve()
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
|
||||
this.destroy()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
|
||||
this.list()
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated user can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
NamedMapsAdminController.prototype.checkContentType = function (action, label) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if (!req.is('application/json')) {
|
||||
const error = new Error(`template ${action} data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.create = function () {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
this.templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
res.status(200);
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function () {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function () {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.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 tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
delete template.auth_id;
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.destroy = function () {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
res.status(204);
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]('');
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function () {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
this.templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_ids: templateIds });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ module.exports = function errorMiddleware (/* options */) {
|
||||
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
|
||||
setErrorHeader(allErrors, statusCode, res);
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
@@ -160,3 +161,53 @@ function errorMessageWithContext(err) {
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
function setErrorHeader(errors, statusCode, res) {
|
||||
let errorsCopy = errors.slice(0);
|
||||
const mainError = errorsCopy.shift();
|
||||
|
||||
let errorsLog = {
|
||||
mainError: {
|
||||
statusCode: statusCode || 200,
|
||||
message: mainError.message,
|
||||
name: mainError.name,
|
||||
label: mainError.label,
|
||||
type: mainError.type,
|
||||
subtype: mainError.subtype
|
||||
}
|
||||
};
|
||||
|
||||
errorsLog.moreErrors = errorsCopy.map(error => {
|
||||
return {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
};
|
||||
});
|
||||
|
||||
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove problematic nested characters
|
||||
* from object for logs RegEx
|
||||
*
|
||||
* @param {Object} object
|
||||
*/
|
||||
function stringifyForLogs(object) {
|
||||
Object.keys(object).map(key => {
|
||||
if(typeof object[key] === 'string') {
|
||||
object[key] = object[key].replace(/[^a-zA-Z0-9]/g, ' ');
|
||||
} else if (typeof object[key] === 'object') {
|
||||
stringifyForLogs(object[key]);
|
||||
} else if (object[key] instanceof Array) {
|
||||
for (let element of object[key]) {
|
||||
stringifyForLogs(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
233
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
233
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
@@ -0,0 +1,233 @@
|
||||
const MapConfig = require('windshaft').model.MapConfig;
|
||||
const aggregationQuery = require('./aggregation-query');
|
||||
const aggregationValidator = require('./aggregation-validator');
|
||||
const {
|
||||
createPositiveNumberValidator,
|
||||
createIncludesValueValidator,
|
||||
createAggregationColumnsValidator
|
||||
} = aggregationValidator;
|
||||
|
||||
const SubstitutionTokens = require('../../utils/substitution-tokens');
|
||||
|
||||
const removeDuplicates = arr => [...new Set(arr)];
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = class AggregationMapConfig extends MapConfig {
|
||||
static get AGGREGATIONS () {
|
||||
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
|
||||
}
|
||||
|
||||
static get PLACEMENTS () {
|
||||
return aggregationQuery.SUPPORTED_PLACEMENTS;
|
||||
}
|
||||
|
||||
static get THRESHOLD () {
|
||||
return 1e5; // 100K
|
||||
}
|
||||
|
||||
static get RESOLUTION () {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static get SUPPORTED_GEOMETRY_TYPES () {
|
||||
return [
|
||||
'ST_Point'
|
||||
];
|
||||
}
|
||||
|
||||
static supportsGeometryType(geometryType) {
|
||||
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
|
||||
}
|
||||
|
||||
static getAggregationGeometryColumn() {
|
||||
return aggregationQuery.GEOMETRY_COLUMN;
|
||||
}
|
||||
|
||||
constructor (user, config, connection, datasource) {
|
||||
super(config, datasource);
|
||||
|
||||
const validate = aggregationValidator(this);
|
||||
const positiveNumberValidator = createPositiveNumberValidator(this);
|
||||
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
|
||||
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
|
||||
|
||||
validate('resolution', positiveNumberValidator);
|
||||
validate('placement', includesValidPlacementsValidator);
|
||||
validate('threshold', positiveNumberValidator);
|
||||
validate('columns', aggregationColumnsValidator);
|
||||
|
||||
this.user = user;
|
||||
this.pgConnection = connection;
|
||||
}
|
||||
|
||||
getAggregatedQuery (index) {
|
||||
const { sql_raw, sql } = this.getLayer(index).options;
|
||||
const {
|
||||
// The default aggregation has no placement, columns or dimensions;
|
||||
// this enables the special "full-sample" aggregation.
|
||||
resolution = AggregationMapConfig.RESOLUTION,
|
||||
threshold = AggregationMapConfig.THRESHOLD,
|
||||
placement,
|
||||
columns = {},
|
||||
dimensions = {}
|
||||
} = this.getAggregation(index);
|
||||
|
||||
return aggregationQuery({
|
||||
query: sql_raw || sql,
|
||||
resolution,
|
||||
threshold,
|
||||
placement,
|
||||
columns,
|
||||
dimensions,
|
||||
isDefaultAggregation: this._isDefaultLayerAggregation(index)
|
||||
});
|
||||
}
|
||||
|
||||
isAggregationMapConfig () {
|
||||
return this.isVectorOnlyMapConfig() || this.hasAnyLayerAggregation();
|
||||
}
|
||||
|
||||
isAggregationLayer (index) {
|
||||
return this.isVectorOnlyMapConfig() || this.hasLayerAggregation(index);
|
||||
}
|
||||
|
||||
hasAnyLayerAggregation () {
|
||||
const layers = this.getLayers();
|
||||
|
||||
for (let index = 0; index < layers.length; index++) {
|
||||
if (this.hasLayerAggregation(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasLayerAggregation (index) {
|
||||
const layer = this.getLayer(index);
|
||||
const { aggregation } = layer.options;
|
||||
|
||||
return aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean');
|
||||
}
|
||||
|
||||
getAggregation (index) {
|
||||
if (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { aggregation } = this.getLayer(index).options;
|
||||
|
||||
if (typeof aggregation === 'boolean') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return aggregation;
|
||||
}
|
||||
|
||||
getLayerAggregationColumns (index, callback) {
|
||||
if (this._isDefaultLayerAggregation(index)) {
|
||||
const skipGeoms = true;
|
||||
return this.getLayerColumns(index, skipGeoms, (err, columns) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, columns);
|
||||
});
|
||||
}
|
||||
|
||||
const columns = this._getLayerAggregationRequiredColumns(index);
|
||||
|
||||
return callback(null, columns);
|
||||
}
|
||||
|
||||
_getLayerAggregationRequiredColumns (index) {
|
||||
const { columns, dimensions } = this.getAggregation(index);
|
||||
|
||||
let aggregatedColumns = [];
|
||||
if (columns) {
|
||||
aggregatedColumns = Object.keys(columns)
|
||||
.map(key => columns[key].aggregated_column)
|
||||
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
|
||||
}
|
||||
|
||||
let dimensionsColumns = [];
|
||||
if (dimensions) {
|
||||
dimensionsColumns = Object.keys(dimensions)
|
||||
.map(key => dimensions[key])
|
||||
.filter(dimension => typeof dimension === 'string');
|
||||
}
|
||||
|
||||
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
|
||||
}
|
||||
|
||||
doesLayerReachThreshold(index, featureCount) {
|
||||
const threshold = this.getAggregation(index) && this.getAggregation(index).threshold ?
|
||||
this.getAggregation(index).threshold :
|
||||
AggregationMapConfig.THRESHOLD;
|
||||
|
||||
return featureCount >= threshold;
|
||||
}
|
||||
|
||||
getLayerColumns (index, skipGeoms, callback) {
|
||||
const geomColumns = ['the_geom', 'the_geom_webmercator'];
|
||||
const limitedQuery = ctx => `SELECT * FROM (${ctx.query}) __cdb_schema LIMIT 0`;
|
||||
const layer = this.getLayer(index);
|
||||
|
||||
this.pgConnection.getConnection(this.user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const sql = limitedQuery({
|
||||
query: prepareSql(layer.options.sql)
|
||||
});
|
||||
|
||||
connection.query(sql, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let columns = result.fields || [];
|
||||
|
||||
columns = columns.map(({ name }) => name);
|
||||
|
||||
if (skipGeoms) {
|
||||
columns = columns.filter((column) => !geomColumns.includes(column));
|
||||
}
|
||||
|
||||
return callback(err, columns);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isDefaultLayerAggregation (index) {
|
||||
const aggregation = this.getAggregation(index);
|
||||
|
||||
return (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) ||
|
||||
aggregation === true ||
|
||||
this._isDefaultAggregation(aggregation);
|
||||
}
|
||||
|
||||
_isDefaultAggregation (aggregation) {
|
||||
return aggregation.placement === undefined &&
|
||||
aggregation.columns === undefined &&
|
||||
this._isEmptyParameter(aggregation.dimensions);
|
||||
}
|
||||
|
||||
_isEmptyParameter(parameter) {
|
||||
return parameter === undefined || parameter === null || this._isEmptyObject(parameter);
|
||||
}
|
||||
|
||||
_isEmptyObject (parameter) {
|
||||
return typeof parameter === 'object' && Object.keys(parameter).length === 0;
|
||||
}
|
||||
};
|
||||
253
lib/cartodb/models/aggregation/aggregation-query.js
Normal file
253
lib/cartodb/models/aggregation/aggregation-query.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const DEFAULT_PLACEMENT = 'point-sample';
|
||||
|
||||
/**
|
||||
* Returns a template function (function that accepts template parameters and returns a string)
|
||||
* to generate an aggregation query.
|
||||
* Valid options to define the query template are:
|
||||
* - placement
|
||||
* - columns
|
||||
* - dimensions*
|
||||
* The query template parameters taken by the result template function are:
|
||||
* - sourceQuery
|
||||
* - res
|
||||
* - columns
|
||||
* - dimensions
|
||||
*/
|
||||
const templateForOptions = (options) => {
|
||||
let templateFn = defaultAggregationQueryTemplate;
|
||||
if (!options.isDefaultAggregation) {
|
||||
templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
|
||||
if (!templateFn) {
|
||||
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
|
||||
}
|
||||
}
|
||||
return templateFn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an aggregation query given the aggregation options:
|
||||
* - query
|
||||
* - resolution - defined as in torque:
|
||||
* aggregation cell is resolution*resolution pixels, where tiles are always 256x256 pixels
|
||||
* - columns
|
||||
* - placement
|
||||
* - dimensions
|
||||
*
|
||||
* The default aggregation (when no explicit placement, columns or dimensions are present) returns
|
||||
* a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
|
||||
* When placement, columns or dimensions are specified, columns are aggregated as requested
|
||||
* (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
|
||||
*/
|
||||
const queryForOptions = (options) => templateForOptions(options)({
|
||||
sourceQuery: options.query,
|
||||
res: 256/options.resolution,
|
||||
columns: options.columns,
|
||||
dimensions: options.dimensions
|
||||
});
|
||||
|
||||
module.exports = queryForOptions;
|
||||
|
||||
const SUPPORTED_AGGREGATE_FUNCTIONS = {
|
||||
'count': {
|
||||
sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
|
||||
},
|
||||
'avg': {
|
||||
sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'sum': {
|
||||
sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'min': {
|
||||
sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'max': {
|
||||
sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'mode': {
|
||||
sql: (column_name, params) => `_cdb_mode(${params.aggregated_column || column_name})`
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);
|
||||
|
||||
const sep = (list) => {
|
||||
let expr = list.join(', ');
|
||||
return expr ? ', ' + expr : expr;
|
||||
};
|
||||
|
||||
const aggregateColumns = ctx => {
|
||||
return Object.assign({
|
||||
_cdb_feature_count: {
|
||||
aggregate_function: 'count'
|
||||
}
|
||||
}, ctx.columns || {});
|
||||
};
|
||||
|
||||
const aggregateColumnNames = (ctx, table) => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(columns).map(
|
||||
column_name => `${table}.${column_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(columns));
|
||||
};
|
||||
|
||||
const aggregateColumnDefs = ctx => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
return sep(Object.keys(columns).map(column_name => {
|
||||
const aggregate_function = columns[column_name].aggregate_function || 'count';
|
||||
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
|
||||
if (!aggregate_definition) {
|
||||
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
|
||||
}
|
||||
const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
|
||||
return `${aggregate_expression} AS ${column_name}`;
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const aggregateDimensions = ctx => ctx.dimensions || {};
|
||||
|
||||
const dimensionNames = (ctx, table) => {
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(dimensions).map(
|
||||
dimension_name => `${table}.${dimension_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(dimensions));
|
||||
};
|
||||
|
||||
const dimensionDefs = ctx => {
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
return sep(Object.keys(dimensions).map(dimension_name => {
|
||||
const expression = dimensions[dimension_name];
|
||||
return `${expression} AS ${dimension_name}`;
|
||||
}));
|
||||
};
|
||||
|
||||
// SQL expression to compute the aggregation resolution (grid cell size).
|
||||
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
|
||||
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
|
||||
// (i.e. each tile is divided into ctx.res*ctx.res cells).
|
||||
const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::double precision`;
|
||||
|
||||
// Notes:
|
||||
// * We need to filter spatially using !bbox! to make the queries efficient because
|
||||
// the filter added by Mapnik (wrapping the query)
|
||||
// is only applied after the aggregation.
|
||||
// * This queries are used for rendering and the_geom is omitted in the results for better performance
|
||||
|
||||
// The special default aggregation includes all the columns of a sample row per grid cell and
|
||||
// the count (_cdb_feature_count) of the aggregated rows.
|
||||
const defaultAggregationQueryTemplate = ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
) SELECT
|
||||
_cdb_query.*
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`;
|
||||
|
||||
const aggregationQueryTemplates = {
|
||||
'centroid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
)
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
ST_SetSRID(
|
||||
ST_MakePoint(
|
||||
AVG(ST_X(_cdb_query.the_geom_webmercator)),
|
||||
AVG(ST_Y(_cdb_query.the_geom_webmercator))
|
||||
), 3857
|
||||
) AS the_geom_webmercator
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
`,
|
||||
|
||||
'point-grid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM _cdb_clusters, _cdb_params
|
||||
`,
|
||||
|
||||
'point-sample': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
_cdb_clusters.cartodb_id,
|
||||
the_geom, the_geom_webmercator
|
||||
${dimensionNames(ctx, '_cdb_query')}
|
||||
${aggregateColumnNames(ctx, '_cdb_clusters')}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`
|
||||
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
|
||||
module.exports.GEOMETRY_COLUMN = 'the_geom_webmercator';
|
||||
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
@@ -0,0 +1,93 @@
|
||||
module.exports = function aggregationValidator (mapconfig) {
|
||||
return function validateProperty (key, validator) {
|
||||
for (let index = 0; index < mapconfig.getLayers().length; index++) {
|
||||
const aggregation = mapconfig.getAggregation(index);
|
||||
|
||||
if (aggregation === undefined || aggregation[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validator(aggregation[key], key, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createIncludesValueValidator = function (mapconfig, validValues) {
|
||||
return function validateIncludesValue (value, key, index) {
|
||||
if (!validValues.includes(value)) {
|
||||
const message = `Invalid ${key}. Valid values: ${validValues.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createPositiveNumberValidator = function (mapconfig) {
|
||||
return function validatePositiveNumber (value, key, index) {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
const message = `Invalid ${key}, should be a number greather than 0`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createAggregationColumnsValidator = function (mapconfig, validAggregatedFunctions) {
|
||||
const validateAggregationColumnNames = createAggregationColumnNamesValidator(mapconfig);
|
||||
const validateAggregateFunction = createAggregateFunctionValidator(mapconfig, validAggregatedFunctions);
|
||||
const validateAggregatedColumn = createAggregatedColumnValidator(mapconfig);
|
||||
|
||||
return function validateAggregationColumns (value, key, index) {
|
||||
validateAggregationColumnNames(value, key, index);
|
||||
validateAggregateFunction(value, key, index);
|
||||
validateAggregatedColumn(value, key, index);
|
||||
};
|
||||
};
|
||||
|
||||
function createAggregationColumnNamesValidator(mapconfig) {
|
||||
return function validateAggregationColumnNames (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
if (columnName.length <= 0) {
|
||||
const message = `Invalid column name, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregateFunctionValidator (mapconfig, validAggregatedFunctions) {
|
||||
return function validateAggregateFunction (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregate_function } = value[columnName];
|
||||
|
||||
if (!validAggregatedFunctions.includes(aggregate_function)) {
|
||||
const message = `Unsupported aggregation function ${aggregate_function},` +
|
||||
` valid ones: ${validAggregatedFunctions.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregatedColumnValidator (mapconfig) {
|
||||
return function validateAggregatedColumn (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregated_column } = value[columnName];
|
||||
|
||||
if (typeof aggregated_column !== 'string' || aggregated_column <= 0) {
|
||||
const message = `Invalid aggregated column, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createLayerError(message, mapconfig, index) {
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapconfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapconfig.layerType(index)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
|
||||
|
||||
const rankedAggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(category AS text),
|
||||
category,
|
||||
value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
@@ -87,7 +87,7 @@ const rankedAggregationQueryTpl = ctx => `
|
||||
WHERE rank < ${ctx.limit}
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Other' category,
|
||||
null category,
|
||||
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
|
||||
true as agg,
|
||||
nulls_count,
|
||||
@@ -109,7 +109,7 @@ const rankedAggregationQueryTpl = ctx => `
|
||||
|
||||
const aggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(${ctx.column} AS text) AS category,
|
||||
${ctx.column} AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
@@ -120,7 +120,7 @@ const aggregationQueryTpl = ctx => `
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
|
||||
GROUP BY
|
||||
category,
|
||||
${ctx.column},
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
@@ -282,7 +282,7 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
max_val = 0,
|
||||
categories_count = 0
|
||||
} = result.rows[0] || {};
|
||||
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
@@ -292,7 +292,13 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
min: min_val,
|
||||
max: max_val,
|
||||
categoriesCount: categories_count,
|
||||
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
|
||||
categories: result.rows.map(({ category, value, agg }) => {
|
||||
return {
|
||||
category: agg ? 'Other' : category,
|
||||
value,
|
||||
agg
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,14 @@
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:formula');
|
||||
const utils = require('../../utils/query-utils');
|
||||
|
||||
const countInfinitiesQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
|
||||
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
|
||||
`;
|
||||
|
||||
const countNansQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
`;
|
||||
|
||||
const filterOutSpecialNumericValuesTpl = ctx => `
|
||||
WHERE
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const formulaQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.operation}(${ctx.column}) AS result,
|
||||
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
|
||||
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
|
||||
FROM (${ctx.query}) __cdb_formula
|
||||
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
|
||||
`;
|
||||
const formulaQueryTpl = ctx =>
|
||||
`SELECT
|
||||
${ctx.operation}(${utils.handleFloatColumn(ctx)}) AS result,
|
||||
${utils.countNULLs(ctx)} AS nulls_count
|
||||
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
|
||||
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
|
||||
FROM (${ctx.query}) __cdb_formula`;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
count: true,
|
||||
|
||||
@@ -1,5 +1,102 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:date-histogram');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
/**
|
||||
* Gets the name of a timezone with the same offset as the required
|
||||
* using the pg_timezone_names table. We do this because it's simpler to pass
|
||||
* the name than to pass the offset itself as PostgreSQL uses different
|
||||
* sign convention. For example: TIME ZONE 'CET' is equal to TIME ZONE 'UTC-1',
|
||||
* not 'UTC+1' which would be expected.
|
||||
* Gives priority to Etc/GMT±N timezones but still support odd offsets like 8.5
|
||||
* hours for Asia/Pyongyang.
|
||||
* It also makes it easier to, in the future, support the input of expected timezone
|
||||
* instead of the offset; that is using 'Europe/Madrid' instead of
|
||||
* '+3600' or '+7200'. The daylight saving status can be handled by postgres.
|
||||
*/
|
||||
const offsetNameQueryTpl = ctx => `
|
||||
WITH __wd_tz AS
|
||||
(
|
||||
SELECT name
|
||||
FROM pg_timezone_names
|
||||
WHERE utc_offset = interval '${ctx.offset} hours'
|
||||
ORDER BY CASE WHEN name LIKE 'Etc/GMT%' THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
),`;
|
||||
|
||||
/**
|
||||
* Function to get the subquery that places each row in its bin depending on
|
||||
* the aggregation. Since the data stored is in epoch we need to adapt it to
|
||||
* our timezone so when calling date_trunc it falls into the correct bin
|
||||
*/
|
||||
function dataBucketsQuery(ctx) {
|
||||
var condition_str = '';
|
||||
|
||||
if (ctx.start !== 0) {
|
||||
condition_str = `WHERE ${ctx.column} >= to_timestamp(${ctx.start})`;
|
||||
}
|
||||
if (ctx.end !== 0) {
|
||||
if (condition_str === '') {
|
||||
condition_str = `WHERE ${ctx.column} <= to_timestamp(${ctx.end})`;
|
||||
}
|
||||
else {
|
||||
condition_str += ` and ${ctx.column} <= to_timestamp(${ctx.end})`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
__wd_buckets AS
|
||||
(
|
||||
SELECT
|
||||
date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, ${ctx.column}::timestamptz)) as timestamp,
|
||||
count(*) as freq,
|
||||
${utils.countNULLs(ctx)} as nulls_count
|
||||
FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __source, __wd_tz
|
||||
${condition_str}
|
||||
GROUP BY 1, __wd_tz.name
|
||||
),`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that generates an array with all the possible bins between the
|
||||
* start and end date. If not provided we use the min and max generated from
|
||||
* the dataBucketsQuery
|
||||
*/
|
||||
function allBucketsArrayQuery(ctx) {
|
||||
var extra_from = ``;
|
||||
var series_start = ``;
|
||||
var series_end = ``;
|
||||
|
||||
if (ctx.start === 0) {
|
||||
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
|
||||
series_start = `min(__wd_buckets.timestamp)`;
|
||||
} else {
|
||||
series_start = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.start})))`;
|
||||
}
|
||||
|
||||
if (ctx.end === 0) {
|
||||
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
|
||||
series_end = `max(__wd_buckets.timestamp)`;
|
||||
} else {
|
||||
series_end = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.end})))`;
|
||||
}
|
||||
|
||||
return `
|
||||
__wd_all_buckets AS
|
||||
(
|
||||
SELECT ARRAY(
|
||||
SELECT
|
||||
generate_series(
|
||||
${series_start},
|
||||
${series_end},
|
||||
interval '${ctx.interval}') as bin_start
|
||||
FROM __wd_tz${extra_from}
|
||||
) as bins
|
||||
)`;
|
||||
}
|
||||
|
||||
const dateIntervalQueryTpl = ctx => `
|
||||
WITH
|
||||
@@ -30,8 +127,11 @@ const dateIntervalQueryTpl = ctx => `
|
||||
FROM __cdb_interval_in_minutes, __cdb_dates
|
||||
)
|
||||
SELECT
|
||||
ROUND(__cdb_days / 365243) AS millennium,
|
||||
ROUND(__cdb_days / 36525) AS century,
|
||||
ROUND(__cdb_days / 3652) AS decade,
|
||||
ROUND(__cdb_days / 365) AS year,
|
||||
ROUND(__cdb_days / 90) AS quarter,
|
||||
ROUND(__cdb_days / 91) AS quarter,
|
||||
ROUND(__cdb_days / 30) AS month,
|
||||
ROUND(__cdb_days / 7) AS week,
|
||||
__cdb_days AS day,
|
||||
@@ -41,119 +141,22 @@ const dateIntervalQueryTpl = ctx => `
|
||||
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
|
||||
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateOverrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end})::float AS __cdb_max_val,
|
||||
min(${ctx.start})::float AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}',
|
||||
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(
|
||||
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
__cdb_bins_array,
|
||||
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
|
||||
FROM (
|
||||
SELECT
|
||||
ARRAY(
|
||||
SELECT GENERATE_SERIES(
|
||||
__cdb_start_date::timestamptz,
|
||||
__cdb_end_date::timestamptz,
|
||||
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
|
||||
)
|
||||
) AS __cdb_bins_array
|
||||
FROM __cdb_basics
|
||||
) __cdb_bins_array_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateHistogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(
|
||||
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
|
||||
__cdb_bins_array
|
||||
),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(
|
||||
date_part(
|
||||
'epoch',
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
)::numeric AS timestamp,
|
||||
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
|
||||
min(date_part('epoch', ${ctx.column}))::numeric AS min,
|
||||
max(date_part('epoch', ${ctx.column}))::numeric AS max,
|
||||
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
|
||||
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const MAX_INTERVAL_VALUE = 366;
|
||||
/** Constant to switch between aggregations in auto mode */
|
||||
const MAX_INTERVAL_VALUE = 100;
|
||||
|
||||
const DATE_AGGREGATIONS = {
|
||||
'auto': true,
|
||||
'second' : true,
|
||||
'minute': true,
|
||||
'hour': true,
|
||||
'day': true,
|
||||
'week': true,
|
||||
'month': true,
|
||||
'quarter': true,
|
||||
'year': true
|
||||
'year': true,
|
||||
'decade' : true,
|
||||
'century' : true,
|
||||
'millennium' : true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -176,12 +179,21 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
|
||||
${dateBinsQueryTpl(ctx)},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${dateHistogramQueryTpl(ctx)}
|
||||
`;
|
||||
${offsetNameQueryTpl(ctx)}
|
||||
${dataBucketsQuery(ctx)}
|
||||
${allBucketsArrayQuery(ctx)}
|
||||
SELECT
|
||||
array_position(__wd_all_buckets.bins, __wd_buckets.timestamp) - 1 as bin,
|
||||
date_part('epoch', timezone(__wd_tz.name, __wd_buckets.timestamp)) AS timestamp,
|
||||
__wd_buckets.freq as freq,
|
||||
date_part('epoch', timezone(__wd_tz.name, (__wd_all_buckets.bins)[1])) as timestamp_start,
|
||||
array_length(__wd_all_buckets.bins, 1) as bins_number,
|
||||
date_part('epoch', interval '${ctx.interval}') as bin_width,
|
||||
__wd_buckets.nulls_count as nulls_count
|
||||
FROM __wd_buckets, __wd_all_buckets, __wd_tz
|
||||
GROUP BY __wd_tz.name, __wd_all_buckets.bins, __wd_buckets.timestamp, __wd_buckets.nulls_count, __wd_buckets.freq
|
||||
ORDER BY bin ASC;
|
||||
`;
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
@@ -204,6 +216,9 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
return null;
|
||||
}
|
||||
|
||||
var interval = this._getAggregation(override) === 'quarter' ?
|
||||
'3 months' : '1 ' + this._getAggregation(override);
|
||||
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
@@ -211,7 +226,8 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
aggregation: this._getAggregation(override),
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
offset: this._parseOffset(override)
|
||||
offset: this._parseOffset(override),
|
||||
interval: interval
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
@@ -234,9 +250,9 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const aggegations = result.rows[0];
|
||||
const aggregation = Object.keys(aggegations)
|
||||
.map(key => ({ name: key, value: aggegations[key] }))
|
||||
const aggregations = result.rows[0];
|
||||
const aggregation = Object.keys(aggregations)
|
||||
.map(key => ({ name: key, value: aggregations[key] }))
|
||||
.reduce((closer, current) => {
|
||||
if (current.value > MAX_INTERVAL_VALUE) {
|
||||
return closer;
|
||||
@@ -264,8 +280,8 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
offset: this._getOffset(override),
|
||||
timestamp_start: firstRow.timestamp_start,
|
||||
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bin_width: firstRow.bin_width || 0,
|
||||
bins_count: firstRow.bins_number || 0,
|
||||
bins_start: firstRow.timestamp,
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
@@ -275,6 +291,10 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
result.rows.forEach(function(row) {
|
||||
row.min = row.max = row.avg = row.timestamp;
|
||||
});
|
||||
|
||||
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,25 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:numeric-histogram');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
|
||||
|
||||
const filterOutSpecialNumericValues = ctx => `
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
/** Query to get min and max values from the query */
|
||||
const irqQueryTpl = ctx => `
|
||||
__cdb_filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) __cdb_filtered_source_query
|
||||
WHERE ${ctx.column} IS NOT NULL
|
||||
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
|
||||
)
|
||||
`;
|
||||
|
||||
const basicsQueryTpl = ctx => `
|
||||
WHERE ${utils.handleFloatColumn(ctx)} IS NOT NULL
|
||||
),
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
max(${ctx.column}) AS __cdb_max_val,
|
||||
min(${ctx.column}) AS __cdb_min_val,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const iqrQueryTpl = ctx => `
|
||||
/* Query to calculate the number of bins (needs irqQueryTpl before it*/
|
||||
const binsQueryTpl = ctx => `
|
||||
__cdb_iqrange AS (
|
||||
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
|
||||
FROM (
|
||||
@@ -47,12 +28,9 @@ const iqrQueryTpl = ctx => `
|
||||
) AS quartile
|
||||
FROM __cdb_filtered_source) _cdb_quartiles
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
GROUP BY 1
|
||||
) __cdb_iqr
|
||||
)
|
||||
`;
|
||||
|
||||
const binsQueryTpl = ctx => `
|
||||
),
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
|
||||
@@ -70,83 +48,6 @@ const binsQueryTpl = ctx => `
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT ${ctx.override.bins} AS __cdb_bins_number
|
||||
)
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const infinitiesQueryTpl = ctx => `
|
||||
__cdb_infinities AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_infinities_count
|
||||
FROM (${ctx.query}) __cdb_infinities_query
|
||||
WHERE
|
||||
${ctx.column} = 'infinity'::float
|
||||
OR
|
||||
${ctx.column} = '-infinity'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const nansQueryTpl = ctx => `
|
||||
__cdb_nans AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nans_count
|
||||
FROM (${ctx.query}) __cdb_nans_query
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnDefinitionTpl = () => `
|
||||
__cdb_infinities_count AS infinities_count,
|
||||
__cdb_nans_count AS nans_count
|
||||
`;
|
||||
|
||||
const specialNumericValuesCTETpl = () => `
|
||||
__cdb_infinities, __cdb_nans
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnTpl = () => `
|
||||
infinities_count, nans_count
|
||||
`;
|
||||
|
||||
const histogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
|
||||
__cdb_avg_val AS avg_val,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(${ctx.column})::numeric AS min,
|
||||
max(${ctx.column})::numeric AS max,
|
||||
avg(${ctx.column})::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const BIN_MIN_NUMBER = 6;
|
||||
const BIN_MAX_NUMBER = 48;
|
||||
|
||||
@@ -167,14 +68,14 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
|
||||
column: this._columnType === 'date' ? utils.columnCastTpl({ column: this.column }) : this.column,
|
||||
isFloatColumn: this._columnType === 'float',
|
||||
query: this.query,
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
bins: this._getBinsCount(override),
|
||||
minBins: BIN_MIN_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
@@ -182,19 +83,62 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ctx: Object with the following values
|
||||
* ctx.column -- Column for the histogram
|
||||
* ctx.isFloatColumn - Whether the column is float or not
|
||||
* ctx.query -- Subquery to extract data
|
||||
* ctx.start -- Start value for the bins. [>= end to force calculation]
|
||||
* ctx.end -- End value for the bins.
|
||||
* ctx.bins -- Numbers of bins to generate [<0 to force calculation]
|
||||
* ctx.minBins - If !full min bins to calculate [Optional]
|
||||
* ctx.maxBins - If !full max bins to calculate [Optional]
|
||||
*/
|
||||
_buildQueryTpl (ctx) {
|
||||
var extra_tables = ``;
|
||||
var extra_queries = ``;
|
||||
var extra_groupby = ``;
|
||||
|
||||
if (ctx.start >= ctx.end) {
|
||||
ctx.end = `__cdb_basics.__cdb_max_val`;
|
||||
ctx.start = `__cdb_basics.__cdb_min_val`;
|
||||
extra_groupby = `, __cdb_basics.__cdb_max_val, __cdb_basics.__cdb_min_val`;
|
||||
extra_tables = `, __cdb_basics`;
|
||||
extra_queries = `WITH ${irqQueryTpl(ctx)}`;
|
||||
}
|
||||
|
||||
if (ctx.bins <= 0) {
|
||||
ctx.bins = `__cdb_bins.__cdb_bins_number`;
|
||||
extra_groupby += `, __cdb_bins.__cdb_bins_number`;
|
||||
extra_tables += `, __cdb_bins`;
|
||||
extra_queries = `WITH ${irqQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`;
|
||||
}
|
||||
|
||||
return `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
|
||||
${this._hasOverridenBins(ctx.override) ?
|
||||
overrideBinsQueryTpl(ctx) :
|
||||
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
|
||||
},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
|
||||
${histogramQueryTpl(ctx)}
|
||||
`;
|
||||
${extra_queries}
|
||||
SELECT
|
||||
(${ctx.end} - ${ctx.start}) / ${ctx.bins}::float AS bin_width,
|
||||
${ctx.bins} as bins_number,
|
||||
${utils.countNULLs(ctx)} AS nulls_count,
|
||||
${utils.countInfinites(ctx)} AS infinities_count,
|
||||
${utils.countNaNs(ctx)} AS nans_count,
|
||||
min(${utils.handleFloatColumn(ctx)}) AS min,
|
||||
max(${utils.handleFloatColumn(ctx)}) AS max,
|
||||
avg(${utils.handleFloatColumn(ctx)}) AS avg,
|
||||
sum(CASE WHEN (${utils.handleFloatColumn(ctx)} is not NULL) THEN 1 ELSE 0 END) as freq,
|
||||
CASE WHEN ${ctx.start} = ${ctx.end}
|
||||
THEN 0
|
||||
ELSE GREATEST(1, LEAST(
|
||||
${ctx.bins},
|
||||
WIDTH_BUCKET(${utils.handleFloatColumn(ctx)}, ${ctx.start}, ${ctx.end}, ${ctx.bins}))) - 1
|
||||
END AS bin
|
||||
FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __cdb_filtered_source_query${extra_tables}
|
||||
GROUP BY 10${extra_groupby}
|
||||
ORDER BY 10;`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
@@ -204,14 +148,31 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
var total_nulls = 0;
|
||||
var total_infinities = 0;
|
||||
var total_nans = 0;
|
||||
var total_avg = 0;
|
||||
var total_count = 0;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
total_nulls += row.nulls_count;
|
||||
total_infinities += row.infinities_count;
|
||||
total_nans += row.nans_count;
|
||||
total_avg += row.avg * row.freq;
|
||||
total_count += row.freq;
|
||||
});
|
||||
if (total_count !== 0) {
|
||||
total_avg /= total_count;
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: this._populateBinStart(firstRow, override),
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val,
|
||||
nulls: total_nulls,
|
||||
infinities: total_infinities,
|
||||
nans: total_nans,
|
||||
avg: total_avg
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,38 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
var debug = require('debug')('windshaft:widget:formula:overview');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
const VALID_OPERATIONS = {
|
||||
count: true,
|
||||
sum: true,
|
||||
avg: true
|
||||
};
|
||||
|
||||
/** Formulae to calculate the end result using _feature_count from the overview table*/
|
||||
function dataviewResult(ctx) {
|
||||
switch (ctx.operation) {
|
||||
case 'count':
|
||||
return `sum(_feature_count)`;
|
||||
case 'sum':
|
||||
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)`;
|
||||
case 'avg':
|
||||
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)/sum(_feature_count) `;
|
||||
}
|
||||
return `${ctx.operation}(${utils.handleFloatColumn(ctx)})`;
|
||||
}
|
||||
|
||||
const formulaQueryTpl = ctx =>
|
||||
`SELECT
|
||||
${dataviewResult(ctx)} AS result,
|
||||
${utils.countNULLs(ctx)} AS nulls_count
|
||||
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
|
||||
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
|
||||
FROM (${ctx.query}) __cdb_formula`;
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
this.column = options.column || '1';
|
||||
@@ -65,36 +48,31 @@ module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function (psql, override, callback) {
|
||||
var self = this;
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if (formulaQueryTpl) {
|
||||
// supported formula for use with overviews
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
if (!VALID_OPERATIONS[this.operation]) {
|
||||
return this.defaultSql(psql, override, callback);
|
||||
}
|
||||
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, override, callback);
|
||||
var formulaSql = formulaQueryTpl({
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
query: this.rewrittenQuery(this.query),
|
||||
operation: this.operation,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
const AggregationMapConfig = require('../../aggregation/aggregation-mapconfig');
|
||||
const queryUtils = require('../../../utils/query-utils');
|
||||
|
||||
const unsupportedGeometryTypeErrorMessage = ctx =>
|
||||
`Unsupported geometry type: ${ctx.geometryType}. ` +
|
||||
`Aggregation is available only for geometry type: ${AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES}`;
|
||||
|
||||
const invalidAggregationParamValueErrorMessage = ctx =>
|
||||
`Invalid value for 'aggregation' query param: ${ctx.value}. Valid ones are 'true' or 'false'`;
|
||||
|
||||
module.exports = class AggregationMapConfigAdapter {
|
||||
constructor (pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
getMapConfig (user, requestMapConfig, params, context, callback) {
|
||||
if (!this._isValidAggregationQueryParam(params)) {
|
||||
return callback(new Error(invalidAggregationParamValueErrorMessage({ value: params.aggregation })));
|
||||
}
|
||||
|
||||
let mapConfig;
|
||||
try {
|
||||
mapConfig = new AggregationMapConfig(user, requestMapConfig, this.pgConnection);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
||||
if (!this._shouldAdapt(mapConfig, params)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this._adaptLayers(connection, mapConfig, requestMapConfig, context, callback);
|
||||
});
|
||||
}
|
||||
|
||||
_isValidAggregationQueryParam (params) {
|
||||
const { aggregation } = params;
|
||||
return aggregation === undefined || aggregation === 'true' || aggregation === 'false';
|
||||
}
|
||||
|
||||
_shouldAdapt (mapConfig, params) {
|
||||
const { aggregation } = params;
|
||||
|
||||
if (aggregation === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aggregation === 'true' || mapConfig.isAggregationMapConfig()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_adaptLayers (connection, mapConfig, requestMapConfig, context, callback) {
|
||||
const adaptLayerPromises = requestMapConfig.layers.map((layer, index) => {
|
||||
return this._adaptLayer(connection, mapConfig, layer, index);
|
||||
});
|
||||
|
||||
Promise.all(adaptLayerPromises)
|
||||
.then(results => {
|
||||
context.aggregation = {
|
||||
layers: []
|
||||
};
|
||||
|
||||
results.forEach(({ layer, index, adapted }) => {
|
||||
if (adapted) {
|
||||
requestMapConfig.layers[index] = layer;
|
||||
}
|
||||
const aggregatedFormats = this._getAggregationMetadata(mapConfig, layer, adapted);
|
||||
context.aggregation.layers.push(aggregatedFormats);
|
||||
});
|
||||
|
||||
callback(null, requestMapConfig);
|
||||
})
|
||||
.catch(err => callback(err));
|
||||
}
|
||||
|
||||
_adaptLayer (connection, mapConfig, layer, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._shouldAdaptLayer(connection, mapConfig, layer, index, (err, shouldAdapt) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!shouldAdapt) {
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
}
|
||||
|
||||
const sqlQueryWrap = layer.options.sql_wrap;
|
||||
|
||||
let aggregationSql = mapConfig.getAggregatedQuery(index);
|
||||
|
||||
if (sqlQueryWrap) {
|
||||
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
|
||||
}
|
||||
|
||||
layer.options.sql = aggregationSql;
|
||||
|
||||
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
layer.options.columns = columns;
|
||||
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_shouldAdaptLayer (connection, mapConfig, layer, index, callback) {
|
||||
if (!mapConfig.isAggregationLayer(index)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const aggregationMetadata = queryUtils.getAggregationMetadata({
|
||||
query: layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql,
|
||||
geometryColumn: AggregationMapConfig.getAggregationGeometryColumn()
|
||||
});
|
||||
|
||||
connection.query(aggregationMetadata, (err, res) => {
|
||||
if (err) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const result = res.rows[0] || {};
|
||||
|
||||
if (!mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
const message = unsupportedGeometryTypeErrorMessage({ geometryType: result.type });
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapConfig.layerType(index)
|
||||
};
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
_getAggregationMetadata (mapConfig, layer, adapted) {
|
||||
if (!adapted) {
|
||||
return { png: false, mvt: false };
|
||||
}
|
||||
|
||||
if (mapConfig.isVectorOnlyMapConfig()) {
|
||||
return { png: false, mvt: true };
|
||||
}
|
||||
|
||||
return { png: true, mvt: true };
|
||||
}
|
||||
};
|
||||
@@ -58,6 +58,13 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||
|
||||
// Expected format for analyses filters
|
||||
// filters = {analyses: {
|
||||
// a1: [{min, max}, {accept, reject}],
|
||||
// b1: [{range, column, min, max}, {category, column, accept, reject}]
|
||||
// }}
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, filters.analyses);
|
||||
|
||||
function createAnalysis(analysisDefinition, done) {
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
|
||||
if (err) {
|
||||
@@ -200,6 +207,7 @@ function dataviewQuery(node, dataviewName, ownFilter) {
|
||||
|
||||
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
var analyses = requestMapConfig.analyses || [];
|
||||
dataviewsFiltersBySourceId = dataviewsFiltersBySourceId || {};
|
||||
|
||||
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);
|
||||
|
||||
@@ -21,69 +21,208 @@ function ResourceLocator(environment) {
|
||||
|
||||
module.exports = ResourceLocator;
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resource) {
|
||||
ResourceLocator.prototype.getTileUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, resource);
|
||||
}
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
|
||||
if (cdnDomain) {
|
||||
const urls = this.getUrlsFromTemplate(username, new TileResource(resourcePath));
|
||||
return {
|
||||
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
|
||||
http: Array.isArray(urls.http) ? urls.http : [urls.http],
|
||||
https: Array.isArray(urls.https) ? urls.https : [urls.https]
|
||||
};
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TileResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
http: [`http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getTemplateUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new TemplateResource(resourcePath), true);
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TemplateResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: {
|
||||
urlTemplate: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`,
|
||||
subdomains: []
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var urls = {};
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
ResourceLocator.prototype.getUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new Resource(resourcePath));
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new Resource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
function urlForTemplate(tpl, username, cdnDomain, resource, templated) {
|
||||
cdnDomain = cdnDomain || {};
|
||||
if (templated) {
|
||||
return {
|
||||
urlTemplate: tpl({
|
||||
cdn_url: (cdnDomain.hasOwnProperty('urlTemplate') ? cdnDomain.urlTemplate : cdnDomain),
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}),
|
||||
subdomains: cdnDomain.subdomains || []
|
||||
};
|
||||
}
|
||||
if (Array.isArray(cdnDomain)) {
|
||||
return cdnDomain.map(d => tpl({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
return tpl({
|
||||
cdn_url: cdnDomain,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource, templated) {
|
||||
var urls = {};
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = urlForTemplate(this.resourcesUrlTemplates.http, username, cdnDomain.http, resource, templated);
|
||||
}
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
urls.https = urlForTemplate(this.resourcesUrlTemplates.https, username, cdnDomain.https, resource, templated);
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
|
||||
class Resource {
|
||||
constructor (resourcePath) {
|
||||
this.resourcePath = resourcePath;
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.resourcePath;
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
if (!subdomains) {
|
||||
return domain;
|
||||
}
|
||||
return domain.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
let urls = getUrl(baseUrl, username, this.resourcePath);
|
||||
if (subdomains) {
|
||||
urls = urls.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
class TileResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
if (!subdomains) {
|
||||
return domain;
|
||||
}
|
||||
return subdomains.map(s => domain.replace('{s}', s));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
if (!subdomains) {
|
||||
return [super.getUrl(baseUrl, username)];
|
||||
}
|
||||
return subdomains.map(subdomain => {
|
||||
return getUrl(baseUrl, username, this.resourcePath)
|
||||
.replace('{s}', subdomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TemplateResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return {
|
||||
urlTemplate: domain,
|
||||
subdomains: subdomains || []
|
||||
};
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
return {
|
||||
urlTemplate: getUrl(baseUrl, username, this.resourcePath),
|
||||
subdomains: subdomains || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl(baseUrl, username, path) {
|
||||
return `${baseUrl}/${username}/api/v1/map/${path}`;
|
||||
}
|
||||
|
||||
function getCdnUrls(serverMetadata, username, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var httpUrls = resource.getUrl(`http://${cdnUrl.http}`, username);
|
||||
var httpsUrls = resource.getUrl(`https://${cdnUrl.https}`, username);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
httpUrls = resource.getUrl(templates.http.url, username, templates.http.subdomains);
|
||||
httpsUrls = resource.getUrl(templates.https.url, username, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: httpUrls,
|
||||
https: httpsUrls,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCdnDomain(serverMetadata, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var http = cdnUrl.http;
|
||||
var https = cdnUrl.https;
|
||||
var httpDomain = resource.getDomain(cdnUrl.http);
|
||||
var httpsDomain = resource.getDomain(cdnUrl.https);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
var httpUrlTemplate = templates.http.url;
|
||||
var httpsUrlTemplate = templates.https.url;
|
||||
http = httpUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.http.subdomains, resource));
|
||||
https = httpsUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.https.subdomains, resource));
|
||||
httpDomain = httpUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpDomain = resource.getDomain(httpDomain, templates.http.subdomains);
|
||||
httpsDomain = httpsUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpsDomain = resource.getDomain(httpsDomain, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: http,
|
||||
https: https,
|
||||
http: httpDomain,
|
||||
https: httpsDomain,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -41,6 +41,7 @@ var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapc
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
|
||||
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
var AggregationMapConfigAdapter = require('./models/mapconfig/adapter/aggregation-mapconfig-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
@@ -190,6 +191,7 @@ module.exports = function(serverOptions) {
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new AggregationMapConfigAdapter(pgConnection),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
19
lib/cartodb/utils/icu_data_env_setter.js
Normal file
19
lib/cartodb/utils/icu_data_env_setter.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
|
||||
// See https://github.com/CartoDB/support/issues/984
|
||||
// CartoCSS properties text-wrap-width/text-wrap-character not working
|
||||
function setICUEnvVariable() {
|
||||
if (process.env.ICU_DATA === undefined) {
|
||||
const regexedPath = '/node_modules/mapnik/lib/binding/*/share/mapnik/icu/';
|
||||
const directory = glob.sync(path.join(__dirname, '../../..', regexedPath));
|
||||
|
||||
if (directory && directory.length > 0) {
|
||||
process.env.ICU_DATA = directory[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = setICUEnvVariable;
|
||||
@@ -21,6 +21,53 @@ module.exports.extractTableNames = function extractTableNames(query) {
|
||||
].join('');
|
||||
};
|
||||
|
||||
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount(\'' + query + '\') as rows';
|
||||
function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount($windshaft$' + query + '$windshaft$) as rows';
|
||||
}
|
||||
module.exports.getQueryRowCount = getQueryRowEstimation;
|
||||
|
||||
module.exports.getAggregationMetadata = ctx => `
|
||||
WITH
|
||||
rowEstimation AS (
|
||||
${getQueryRowEstimation(ctx.query)}
|
||||
),
|
||||
geometryType AS (
|
||||
SELECT ST_GeometryType(${ctx.geometryColumn}) as geom_type
|
||||
FROM (${ctx.query}) AS __cdb_query WHERE ${ctx.geometryColumn} IS NOT NULL LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
rows AS count,
|
||||
geom_type AS type
|
||||
FROM rowEstimation, geometryType;
|
||||
`;
|
||||
|
||||
/** Cast the column to epoch */
|
||||
module.exports.columnCastTpl = function columnCastTpl(ctx) {
|
||||
return `date_part('epoch', ${ctx.column})`;
|
||||
};
|
||||
|
||||
/** If the column type is float, ignore any non numeric result (infinity / NaN) */
|
||||
module.exports.handleFloatColumn = function handleFloatColumn(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `${ctx.column}` :
|
||||
`nullif(nullif(nullif(${ctx.column}, 'infinity'::float), '-infinity'::float), 'NaN'::float)`
|
||||
}`;
|
||||
};
|
||||
|
||||
/** Count NULL appearances */
|
||||
module.exports.countNULLs= function countNULLs(ctx) {
|
||||
return `sum(CASE WHEN (${ctx.column} IS NULL) THEN 1 ELSE 0 END)`;
|
||||
};
|
||||
|
||||
/** Count only infinity (positive and negative) appearances */
|
||||
module.exports.countInfinites = function countInfinites(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `0` :
|
||||
`sum(CASE WHEN (${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
|
||||
/** Count only NaNs appearances*/
|
||||
module.exports.countNaNs = function countNaNs(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `0` :
|
||||
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
|
||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.2.0",
|
||||
"version": "5.0.2",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -19,11 +19,13 @@
|
||||
"Sandro Santilli <strk@vizzuality.com>",
|
||||
"Carlos Matallín <matallo@carto.com>",
|
||||
"Daniel Garcia Aubert <dgaubert@carto.com>",
|
||||
"Mario de Frutos <mario.defrutos@carto.com>"
|
||||
"Mario de Frutos <mario.defrutos@carto.com>",
|
||||
"Ivan Malagon <ivan@carto.com>",
|
||||
"Simon Martin <simon@carto.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.59.4",
|
||||
"camshaft": "0.62.1",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
@@ -31,6 +33,7 @@
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.16.0",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"glob": "^7.1.2",
|
||||
"log4js": "cartodb/log4js-node#cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~2.3.2",
|
||||
@@ -44,7 +47,7 @@
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "4.1.0",
|
||||
"windshaft": "4.2.0",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,9 +66,8 @@
|
||||
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
|
||||
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
|
||||
"docker-pull": "docker pull cartoimages/windshaft-testing",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh && docker ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9",
|
||||
|
||||
1443
test/acceptance/aggregation.js
Normal file
1443
test/acceptance/aggregation.js
Normal file
File diff suppressed because it is too large
Load Diff
150
test/acceptance/analysis/analyses-filters-params.js
Normal file
150
test/acceptance/analysis/analyses-filters-params.js
Normal file
@@ -0,0 +1,150 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-filters-params', () => {
|
||||
|
||||
const CARTOCSS = `#layer {
|
||||
marker-fill-opacity: 1;
|
||||
marker-line-color: white;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a1"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
},
|
||||
pop_min_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_min'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
min: 2e6
|
||||
},
|
||||
pop_min_histogram: {
|
||||
max: 2e6
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
it('should get a filtered histogram dataview with all filters', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
own_filter: 1
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 6);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a filtered histogram dataview with all filters except my own filter', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
own_filter: 0
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 24);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a filtered histogram dataview without filters', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
no_filters: 1
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 48);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if both no_filters and own_filter params are present', function (done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const expectedError = {
|
||||
errors: ['Both own_filter and no_filters cannot be sent in the same request'],
|
||||
errors_with_context: [{
|
||||
type: 'dataview',
|
||||
message: 'Both own_filter and no_filters cannot be sent in the same request'
|
||||
}]
|
||||
};
|
||||
const testParams = Object.assign({}, params, {
|
||||
no_filters: 1,
|
||||
own_filter: 0,
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.deepEqual(dataview, expectedError);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
84
test/acceptance/analysis/analyses-filters.js
Normal file
84
test/acceptance/analysis/analyses-filters.js
Normal file
@@ -0,0 +1,84 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers-dataviews', () => {
|
||||
|
||||
const CARTOCSS = `#layer {
|
||||
marker-fill-opacity: 1;
|
||||
marker-line-color: white;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a1"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it('should get a filtered histogram dataview', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const params = {
|
||||
filters: {
|
||||
analyses: {
|
||||
'a1': [
|
||||
{
|
||||
type: 'range',
|
||||
column: 'pop_max',
|
||||
params: {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 2008000);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,8 @@ describe('analysis-layers-dataviews', function() {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||
|
||||
@@ -174,7 +174,9 @@ describe('analysis-layers', function() {
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getLayergroup(PERMISSION_DENIED_RESPONSE, function(err, layergroupResult) {
|
||||
|
||||
|
||||
testClient.getLayergroup({ response: PERMISSION_DENIED_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(
|
||||
layergroupResult.errors,
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -97,7 +97,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -144,7 +144,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -190,7 +190,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 11111);
|
||||
|
||||
testClient.getLayergroup(AUTH_ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: AUTH_ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -246,7 +246,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -298,7 +298,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -351,7 +351,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -415,7 +415,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('histogram-dataview', function() {
|
||||
it('should fail when invalid dataviews object is provided, string case', function(done) {
|
||||
var mapConfig = createMapConfig("wadus-string");
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "string" type found' ]);
|
||||
@@ -63,7 +63,7 @@ describe('histogram-dataview', function() {
|
||||
it('should fail when invalid dataviews object is provided, array case', function(done) {
|
||||
var mapConfig = createMapConfig([]);
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "array" type found' ]);
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "datetime-histogram-source"
|
||||
"id": "datetime-histogram-source-week"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
@@ -134,7 +134,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
{
|
||||
datetime_histogram: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source'
|
||||
id: 'datetime-histogram-source-week'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -154,9 +154,109 @@ describe('histogram-dataview for date column type', function() {
|
||||
offset: -14400 // EDT Eastern Daylight Time (GMT-4) in seconds
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic: {
|
||||
datetime_histogram_automatic_second: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source'
|
||||
id: 'datetime-histogram-source-second'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_minute: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-minute'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_hour: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-hour'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_day: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-day'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_week: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-week'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_month: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-month'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_quarter: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-quarter'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_year: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-year'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_decade: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-decade'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_century: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-century'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_millennium: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-millennium'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -186,7 +286,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
},
|
||||
minute_histogram: {
|
||||
source: {
|
||||
id: 'minute-histogram-source'
|
||||
id: 'minute-histogram-source-tz'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -197,13 +297,144 @@ describe('histogram-dataview for date column type', function() {
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "datetime-histogram-source",
|
||||
"id": "datetime-histogram-source-second",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp, '1 day'::interval",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 01:00:57'::timestamp,",
|
||||
"'0.9 second'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-minute",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 02:00:57'::timestamp,",
|
||||
"'75 second'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-hour",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-18 02:00:57'::timestamp,",
|
||||
"'47 minutes'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-day",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-04-18 02:00:57'::timestamp,",
|
||||
"'24 hours'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-week",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp,",
|
||||
"'1 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-month",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2010-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-quarter",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2020-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-year",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'1990-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-decade",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'1850-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-century",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'0650-02-15 01:00:00'::timestamp, '1918-04-09 01:00:00'::timestamp,",
|
||||
"'6 years'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-millennium",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'0005-02-15 01:00:00'::timestamp, '12000-04-09 01:00:00'::timestamp,",
|
||||
"'72 years'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -214,8 +445,8 @@ describe('histogram-dataview for date column type', function() {
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamptz, '2008-04-09 01:00:00'::timestamptz, '1 day'::interval",
|
||||
"from generate_series('2007-02-15 01:00:00+00'::timestamptz,",
|
||||
"'2008-04-09 01:00:00+00'::timestamptz, '1 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -233,13 +464,13 @@ describe('histogram-dataview for date column type', function() {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minute-histogram-source",
|
||||
"id": "minute-histogram-source-tz",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 23:50:00'::timestamp, '2007-02-16 00:10:00'::timestamp, '1 minute'::interval",
|
||||
"from generate_series('2007-02-15 23:50:00+00'::timestamptz,",
|
||||
"'2007-02-16 00:10:00+00'::timestamptz, '1 minute'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -256,6 +487,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
}];
|
||||
|
||||
dateHistogramsUseCases.forEach(function (test) {
|
||||
|
||||
it('should create a date histogram aggregated in months (EDT) ' + test.desc, function (done) {
|
||||
var OFFSET_EDT_IN_MINUTES = -4 * 60; // EDT Eastern Daylight Time (GMT-4) in minutes
|
||||
|
||||
@@ -323,7 +555,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 6);
|
||||
assert.equal(dataview.bins_count, 6);
|
||||
dataview.bins.forEach(function (bin) {
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
@@ -335,7 +567,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
it('should cast overridden start and end to float to avoid out of range errors ' + test.desc, function (done) {
|
||||
var params = {
|
||||
start: -2145916800,
|
||||
end: 1009843199
|
||||
end: 1193792400
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
@@ -348,27 +580,6 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return same histogram ' + test.desc, function (done) {
|
||||
var params = {
|
||||
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
|
||||
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview(test.dataviewId, {}, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview(test.dataviewId, params, function (err, filteredDataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(dataview, filteredDataview);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should aggregate histogram overriding default offset to CEST ' + test.desc, function (done) {
|
||||
var OFFSET_CEST_IN_SECONDS = 2 * 3600; // Central European Summer Time (Daylight Saving Time)
|
||||
var OFFSET_CEST_IN_MINUTES = 2 * 60; // Central European Summer Time (Daylight Saving Time)
|
||||
@@ -449,6 +660,47 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "second" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'second'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 57);
|
||||
|
||||
var initialTimestamp = '2007-02-15T01:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index, 'second')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "quarter" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
@@ -490,6 +742,132 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "decade" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'decade'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 17);
|
||||
|
||||
var initialTimestamp = '1850-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 10, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "century" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'century'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 14);
|
||||
|
||||
var initialTimestamp = '0601-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 100, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "millennium" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'millennium'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 12);
|
||||
|
||||
var initialTimestamp = '0001-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 1000, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
it('bins_count should be equal to bins length filtered by start and end ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var params = {
|
||||
@@ -533,19 +911,149 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram', function (done) {
|
||||
it('should return same histogram ', function (done) {
|
||||
var params = {
|
||||
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
|
||||
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_tz', {}, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_tz', params, function (err, filteredDataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(dataview, filteredDataview);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: second', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic', params, function (err, dataview) {
|
||||
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
assert.equal(dataview.bins.length, 61);
|
||||
assert.equal(dataview.bins_count, 61);
|
||||
assert.equal(dataview.aggregation, 'second');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: minute', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_minute', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'minute');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: hour', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_hour', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'hour');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: day', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_day', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'day');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: week', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_week', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: month', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_month', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'month');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: quarter', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_quarter', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'quarter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: year', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_year', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'year');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: decade', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'decade');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: century', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'century');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: millennium', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'millennium');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('should work with dates', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
@@ -560,19 +1068,6 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram with dates', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('date_histogram_automatic', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
assert.equal(dataview.bins.length, 61);
|
||||
assert.equal(dataview.bins_count, 61);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply offset for a histogram aggregated by minutes', function (done) {
|
||||
var self = this;
|
||||
var params = {
|
||||
@@ -640,7 +1135,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
|
||||
var dataviewWithDailyAggFixture = {
|
||||
aggregation: 'day',
|
||||
bin_width: 600,
|
||||
bin_width: 86400,
|
||||
bins_count: 2,
|
||||
bins_start: 1171497600,
|
||||
timestamp_start: 1171497600,
|
||||
@@ -650,17 +1145,17 @@ describe('histogram-dataview for date column type', function() {
|
||||
[{
|
||||
bin: 0,
|
||||
timestamp: 1171497600,
|
||||
min: 1171583400,
|
||||
max: 1171583940,
|
||||
avg: 1171583670,
|
||||
min: 1171497600,
|
||||
max: 1171497600,
|
||||
avg: 1171497600,
|
||||
freq: 10
|
||||
},
|
||||
{
|
||||
bin: 1,
|
||||
timestamp: 1171584000,
|
||||
min: 1171584000,
|
||||
max: 1171584600,
|
||||
avg: 1171584300,
|
||||
max: 1171584000,
|
||||
avg: 1171584000,
|
||||
freq: 11
|
||||
}],
|
||||
type: 'histogram'
|
||||
@@ -687,19 +1182,19 @@ describe('histogram-dataview for date column type', function() {
|
||||
|
||||
var dataviewWithDailyAggAndOffsetFixture = {
|
||||
aggregation: 'day',
|
||||
bin_width: 1200,
|
||||
bin_width: 86400,
|
||||
bins_count: 1,
|
||||
bins_start: 1171501200,
|
||||
timestamp_start: 1171497600,
|
||||
timestamp_start: 1171501200,
|
||||
nulls: 0,
|
||||
offset: -3600,
|
||||
bins:
|
||||
[{
|
||||
bin: 0,
|
||||
timestamp: 1171501200,
|
||||
min: 1171583400,
|
||||
max: 1171584600,
|
||||
avg: 1171584000,
|
||||
min: 1171501200,
|
||||
max: 1171501200,
|
||||
avg: 1171501200,
|
||||
freq: 21
|
||||
}],
|
||||
type: 'histogram'
|
||||
@@ -858,13 +1353,15 @@ describe('histogram-dates: aggregation input value', function() {
|
||||
|
||||
assert.deepEqual(dataviewError, {
|
||||
errors: [
|
||||
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
|
||||
'hour, day, week, month, quarter, year, decade, century, millennium'
|
||||
],
|
||||
errors_with_context: [{
|
||||
type: 'unknown',
|
||||
message: [
|
||||
'Invalid aggregation value. ',
|
||||
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Valid ones: auto, second, minute, hour, day, week, month, ' +
|
||||
'quarter, year, decade, century, millennium'
|
||||
].join('')
|
||||
}]
|
||||
});
|
||||
@@ -887,13 +1384,15 @@ describe('histogram-dates: aggregation input value', function() {
|
||||
|
||||
assert.deepEqual(dataviewError, {
|
||||
errors: [
|
||||
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
|
||||
'hour, day, week, month, quarter, year, decade, century, millennium'
|
||||
],
|
||||
errors_with_context: [{
|
||||
type: 'unknown',
|
||||
message: [
|
||||
'Invalid aggregation value. ',
|
||||
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Valid ones: auto, second, minute, hour, day, week, month, ' +
|
||||
'quarter, year, decade, century, millennium'
|
||||
].join('')
|
||||
}]
|
||||
});
|
||||
@@ -966,7 +1465,7 @@ describe('histogram-dates: timestamp starts at epoch', function() {
|
||||
const { aggregation, timestamp_start } = dataview;
|
||||
|
||||
assert.equal(timestamp_start, 0);
|
||||
assert.equal(aggregation, 'month');
|
||||
assert.equal(aggregation, 'quarter');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -393,7 +393,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { min: 2 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
@@ -412,7 +413,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { max: -1 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
@@ -433,7 +435,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram_date: { max: -1 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram_date', params, function (err, histogram) {
|
||||
|
||||
42
test/acceptance/error-middleware.js
Normal file
42
test/acceptance/error-middleware.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('error middleware', function () {
|
||||
it('should returns a errors header', function (done) {
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: "Missing cartocss for layer 0 options",
|
||||
name: "Error",
|
||||
label: "ANONYMOUS LAYERGROUP",
|
||||
type: "layer",
|
||||
},
|
||||
moreErrors: []
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const params = {
|
||||
response: {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(params, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
69
test/acceptance/label-wrap.js
Normal file
69
test/acceptance/label-wrap.js
Normal file
@@ -0,0 +1,69 @@
|
||||
require('../support/test_helper');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var IMAGE_TOLERANCE = 5;
|
||||
|
||||
describe('CartoCSS wrap', function () {
|
||||
const options = {
|
||||
sql: `
|
||||
SELECT
|
||||
5 as cartodb_id,
|
||||
ST_Transform(ST_SetSRID(ST_MakePoint(-57.65625,-15.6230368),4326),3857) as the_geom_webmercator,
|
||||
ST_SetSRID(ST_MakePoint(-57.65625,-15.62303683),4326) as the_geom,
|
||||
'South America' as continent
|
||||
`,
|
||||
cartocss: `
|
||||
#continent_points::labels {
|
||||
text-name: [continent];
|
||||
text-face-name: 'DejaVu Sans Book';
|
||||
text-size: 10;
|
||||
text-fill: lighten(#000,40);
|
||||
text-transform: uppercase;
|
||||
text-wrap-width: 30;
|
||||
text-character-spacing: 2;
|
||||
text-placement: point;
|
||||
text-placement-type: dummy;
|
||||
[zoom >= 3]{
|
||||
text-character-spacing: 2;
|
||||
text-size: 11;
|
||||
}
|
||||
}
|
||||
`,
|
||||
cartocss_version: '3.0.12'
|
||||
};
|
||||
|
||||
const type = 'mapnik';
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type,
|
||||
id: 'layerLabel',
|
||||
options
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function () {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
it("Label should be text-wrapped", function (done) {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(1, 0, 1, { layers: [0] }, (err, res, body) => {
|
||||
var textWrapPath = './test/fixtures/text_wrap.png';
|
||||
assert.imageIsSimilarToFile(body, textWrapPath, IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
54
test/acceptance/layers-filters.js
Normal file
54
test/acceptance/layers-filters.js
Normal file
@@ -0,0 +1,54 @@
|
||||
require('../support/test_helper');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
describe('layers filters', function() {
|
||||
const type = 'mapnik';
|
||||
const sql = 'select * from populated_places_simple_reduced';
|
||||
const cartocss = `#points {
|
||||
marker-fill-opacity: 1.0;
|
||||
marker-line-color: #FFF;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1.0;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
const cartocss_version = '3.0.12';
|
||||
const options = {
|
||||
sql,
|
||||
cartocss,
|
||||
cartocss_version
|
||||
};
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type,
|
||||
id: 'layerA',
|
||||
options
|
||||
},
|
||||
{
|
||||
type,
|
||||
id: 'layerB',
|
||||
options
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
['layerA', 'layerB'].forEach(layer => {
|
||||
it(`should work for individual layer ids: ${layer}`, function (done) {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { layers: layer }, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -19,7 +19,8 @@ describe('named maps static view', function() {
|
||||
var username = 'localhost';
|
||||
var templateName = 'template_with_view';
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
var PNG_IMAGE_TOLERANCE = 20;
|
||||
var JPG_IMAGE_TOLERANCE = 100;
|
||||
|
||||
function createTemplate(view, layers) {
|
||||
return {
|
||||
@@ -92,8 +93,8 @@ describe('named maps static view', function() {
|
||||
});
|
||||
}
|
||||
|
||||
function previewFixture(version) {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
|
||||
function previewFixture(version, format='png') {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.' + format;
|
||||
}
|
||||
|
||||
it('should return an image estimating its bounds based on dataset', function (done) {
|
||||
@@ -103,7 +104,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('estimated'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,7 +123,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -142,7 +143,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -167,7 +168,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -192,7 +193,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ zoom: 3 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -217,7 +218,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -256,7 +257,27 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ layer: 0 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return jpg static map', function (done) {
|
||||
var view = {
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ format: 'jpeg' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center', 'jpeg'),
|
||||
JPG_IMAGE_TOLERANCE, done, 'jpeg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('multilayer error cases', function() {
|
||||
};
|
||||
ServerOptions.afterLayergroupCreateCalls = 0;
|
||||
this.client = new TestClient(layergroup);
|
||||
this.client.getLayergroup({status: 400}, function(err, parsed) {
|
||||
this.client.getLayergroup({ response: { status: 400 } }, function(err, parsed) {
|
||||
assert.ok(!err, err);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
|
||||
@@ -257,6 +257,14 @@ describe('torque boundary points', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var parsed = JSON.parse(res.body);
|
||||
/* Order the JSON first by descending x__uint8 and ascending
|
||||
* y__uint8 */
|
||||
parsed.sort(function(a,b) {
|
||||
if (a.x__uint8 === b.x__uint8) {
|
||||
return (a.y__uint8 > b.y__uint8);
|
||||
}
|
||||
return (a.x__uint8 < b.x__uint8);
|
||||
});
|
||||
|
||||
var i = 0;
|
||||
tileRequest.expects.forEach(function(expected) {
|
||||
@@ -424,7 +432,7 @@ describe('torque boundary points', function() {
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.deepEqual(parsed, [
|
||||
assert.deepEqual(parsed.sort(function(a,b){return a.x__uint8 > b.x__uint8;}), [
|
||||
{
|
||||
x__uint8: 47,
|
||||
y__uint8: 127,
|
||||
@@ -438,7 +446,6 @@ describe('torque boundary points', function() {
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('regressions', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
|
||||
219
test/acceptance/tilejson.js
Normal file
219
test/acceptance/tilejson.js
Normal file
@@ -0,0 +1,219 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('tilejson', function() {
|
||||
|
||||
function tilejsonValidation(tilejson, shouldHaveGrid = false) {
|
||||
assert.equal(tilejson.tilejson, '2.2.0');
|
||||
|
||||
assert.ok(Array.isArray(tilejson.tiles), JSON.stringify(tilejson));
|
||||
assert.ok(tilejson.tiles.length > 0);
|
||||
|
||||
if (shouldHaveGrid) {
|
||||
assert.ok(Array.isArray(tilejson.grids));
|
||||
assert.ok(tilejson.grids.length > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sql = 'SELECT * FROM populated_places_simple_reduced';
|
||||
const cartocss = TestClient.CARTOCSS.POINTS;
|
||||
const cartocss_version = '3.0.12';
|
||||
|
||||
const RASTER_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version
|
||||
}
|
||||
};
|
||||
const RASTER_INTERACTIVITY_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version,
|
||||
interactivity: ['cartodb_id']
|
||||
}
|
||||
};
|
||||
const VECTOR_LAYER = {
|
||||
options: {
|
||||
sql
|
||||
}
|
||||
};
|
||||
const PLAIN_LAYER = {
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#000000'
|
||||
}
|
||||
};
|
||||
|
||||
function mapConfig(layers) {
|
||||
return {
|
||||
version: '1.7.0',
|
||||
layers: Array.isArray(layers) ? layers : [layers]
|
||||
};
|
||||
}
|
||||
|
||||
describe('per layer', function() {
|
||||
it('should expose raster + vector tilejson for raster layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the vector tilejson vector only layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the raster tilejson plain layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(PLAIN_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose grids for the raster layer with interactivity', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_INTERACTIVITY_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
tilejsonValidation(layer.tilejson.vector);
|
||||
tilejsonValidation(layer.tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with several layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig([RASTER_LAYER, RASTER_INTERACTIVITY_LAYER]));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 2);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[0].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.raster);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[1].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('root tilejson', function() {
|
||||
|
||||
it('should expose just the `vector` tilejson and URL when for vector only mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
const url = metadata.url;
|
||||
assert.deepEqual(Object.keys(url), ['vector']);
|
||||
|
||||
assert.ok(url.vector.urlTemplate);
|
||||
assert.ok(url.vector.subdomains);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the `vector` and `raster` tilejson and urls for mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
const url = metadata.url;
|
||||
assert.deepEqual(Object.keys(url), ['vector', 'raster']);
|
||||
|
||||
assert.ok(url.vector.urlTemplate);
|
||||
assert.ok(url.vector.subdomains);
|
||||
|
||||
assert.ok(url.raster.urlTemplate);
|
||||
assert.ok(url.raster.subdomains);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid number of ramp error', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([pop_max], 8, 96, 3, (8,24,96,128))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -65,7 +65,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid column from datasource', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -80,7 +80,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid method from datasource', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue), wadusmethod)'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -95,7 +95,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should fail by falling back to normal carto parser', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([price], (8,24,96), (8,24,96));//(red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -109,7 +109,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('turbo-carto: should return error invalid column from datasource with some context', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -164,7 +164,7 @@ describe('turbo-carto error cases', function() {
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(multipleErrorsMapConfig);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('turbo-carto regressions', function() {
|
||||
].join('\n');
|
||||
|
||||
this.testClient = new TestClient(makeMapconfig('SELECT * FROM test_table_private_1', cartocss));
|
||||
this.testClient.getLayergroup(TestClient.RESPONSE.ERROR, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: TestClient.RESPONSE.ERROR }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(!layergroup.hasOwnProperty('layergroupid'));
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -177,7 +177,7 @@ describe('user database timeout limit', function () {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ describe('user database timeout limit', function () {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -360,7 +360,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -387,7 +387,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -426,16 +426,16 @@ describe('user database timeout limit', function () {
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
|
||||
var tileJSON = tile.toJSON();
|
||||
assert.equal(Array.isArray(tileJSON), true);
|
||||
assert.equal(tileJSON.length, 2);
|
||||
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
|
||||
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
|
||||
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -467,7 +467,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -494,7 +494,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -571,7 +571,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -601,7 +601,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -702,7 +702,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -740,7 +740,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('user render timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
@@ -245,7 +245,7 @@ describe('user render timeout limit', function () {
|
||||
assert.equal(tileJSON.length, 2);
|
||||
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
|
||||
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
|
||||
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -399,4 +399,3 @@ describe('user render timeout limit', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ suites.forEach((suite) => {
|
||||
this.testClient.mapConfig.layers[0].options.cartocss = cartocss;
|
||||
this.testClient.mapConfig.layers[0].options.cartocss_version = cartocssVersion;
|
||||
|
||||
this.testClient.getLayergroup(response, (err, body) => {
|
||||
this.testClient.getLayergroup({ response }, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
BIN
test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg
vendored
Normal file
BIN
test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
test/fixtures/text_wrap.png
vendored
Normal file
BIN
test/fixtures/text_wrap.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
test/fixtures/text_wrap_bad.png
vendored
Normal file
BIN
test/fixtures/text_wrap_bad.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
3
test/results/jpeg/.gitignore
vendored
Normal file
3
test/results/jpeg/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.js
|
||||
@@ -37,7 +37,7 @@ assert.imageBuffersAreSimilar = function(bufferA, bufferB, tolerance, callback)
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, callback);
|
||||
};
|
||||
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback, format='png') {
|
||||
callback = callback || function(err) { assert.ifError(err); };
|
||||
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
|
||||
@@ -46,19 +46,23 @@ assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath
|
||||
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, function(err) {
|
||||
if (err) {
|
||||
var testImageFilePath = randomImagePath();
|
||||
testImage.save(testImageFilePath);
|
||||
var testImageFilePath = randomImagePath(format);
|
||||
testImage.save(testImageFilePath, format);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
}, format);
|
||||
};
|
||||
|
||||
function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
|
||||
function imagesAreSimilar(testImage, referenceImage, tolerance, callback, format='png') {
|
||||
if (testImage.width() !== referenceImage.width() || testImage.height() !== referenceImage.height()) {
|
||||
return callback(new Error('Images are not the same size'));
|
||||
}
|
||||
|
||||
var pixelsDifference = referenceImage.compare(testImage);
|
||||
var options = {};
|
||||
if (format === 'jpeg') {
|
||||
options.alpha = false;
|
||||
}
|
||||
var pixelsDifference = referenceImage.compare(testImage, options);
|
||||
var similarity = pixelsDifference / (referenceImage.width() * referenceImage.height());
|
||||
var tolerancePerMil = (tolerance / 1000);
|
||||
|
||||
@@ -73,8 +77,8 @@ function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function randomImagePath() {
|
||||
return path.resolve('test/results/png/image-test-' + Date.now() + '.png');
|
||||
function randomImagePath(format='png') {
|
||||
return path.resolve('test/results/' + format + '/image-test-' + Date.now() + '.' + format);
|
||||
}
|
||||
|
||||
assert.response = function(server, req, res, callback) {
|
||||
|
||||
@@ -77,7 +77,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
|
||||
|
||||
LOCAL_SQL_SCRIPTS='analysis_catalog windshaft.test gadm4 ported/populated_places_simple_reduced cdb_analysis_check cdb_invalidate_varnish'
|
||||
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ CDB_EstimateRowCount'
|
||||
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ CDB_EstimateRowCount CDB_RectangleGrid'
|
||||
|
||||
CURL_ARGS=""
|
||||
for i in ${REMOTE_SQL_SCRIPTS}
|
||||
@@ -99,7 +99,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
sed -e 's/PARALLEL \= [A-Z]*,/''/g' \
|
||||
-e 's/PARALLEL [A-Z]*/''/g' sql/$i.sql > $TMPFILE
|
||||
mv $TMPFILE sql/$i.sql
|
||||
fi
|
||||
fi
|
||||
cat sql/${i}.sql |
|
||||
sed -e 's/cartodb\./public./g' -e "s/''cartodb''/''public''/g" |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
|
||||
@@ -411,9 +411,13 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var urlParams = {
|
||||
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
|
||||
};
|
||||
var urlParams = {};
|
||||
if (params.hasOwnProperty('no_filters')) {
|
||||
urlParams.no_filters = params.no_filters;
|
||||
}
|
||||
if (params.hasOwnProperty('own_filter')) {
|
||||
urlParams.own_filter = params.own_filter;
|
||||
}
|
||||
|
||||
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function(extraParam) {
|
||||
if (params.hasOwnProperty(extraParam)) {
|
||||
@@ -618,8 +622,19 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
|
||||
var data = templateId ? params.placeholders : self.mapConfig;
|
||||
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (params.aggregation !== undefined) {
|
||||
queryParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
var path = templateId ?
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify(queryParams) :
|
||||
url;
|
||||
|
||||
assert.response(self.server,
|
||||
@@ -647,7 +662,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
);
|
||||
},
|
||||
function getTileResult(err, layergroupId) {
|
||||
// jshint maxcomplexity:12
|
||||
// jshint maxcomplexity:13
|
||||
assert.ifError(err);
|
||||
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
@@ -671,8 +686,14 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
url += [z,x,y].join('/');
|
||||
url += '.' + format;
|
||||
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (Object.keys(queryParams).length) {
|
||||
url += '?' + qs.stringify(queryParams);
|
||||
}
|
||||
|
||||
var request = {
|
||||
@@ -754,12 +775,21 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
TestClient.prototype.getLayergroup = function (params, callback) {
|
||||
// jshint maxcomplexity: 7
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = {
|
||||
callback = params;
|
||||
params = null;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
|
||||
if (!params.response) {
|
||||
params.response = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
@@ -769,8 +799,18 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
|
||||
var url = '/api/v1/map';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (params.aggregation !== undefined) {
|
||||
queryParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
if (Object.keys(queryParams).length) {
|
||||
url += '?' + qs.stringify(queryParams);
|
||||
}
|
||||
|
||||
assert.response(self.server,
|
||||
@@ -783,7 +823,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
expectedResponse,
|
||||
params.response,
|
||||
function(res, err) {
|
||||
var parsedBody;
|
||||
// If there is a response, we are still interested in catching the created keys
|
||||
|
||||
@@ -15,12 +15,14 @@ var redis = require('redis');
|
||||
var nock = require('nock');
|
||||
var log4js = require('log4js');
|
||||
var pg = require('pg');
|
||||
const setICUEnvVariable = require('../../lib/cartodb/utils/icu_data_env_setter');
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
global.environment.name = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
setICUEnvVariable();
|
||||
|
||||
// don't output logs in test environment to reduce noise
|
||||
log4js.configure({ appenders: [] });
|
||||
@@ -75,6 +77,22 @@ function checkSurrogateKey(res, expectedKey) {
|
||||
assert.deepEqual(keys, expectedKeys);
|
||||
}
|
||||
|
||||
var uncaughtExceptions = [];
|
||||
process.on('uncaughtException', function(err) {
|
||||
uncaughtExceptions.push(err);
|
||||
});
|
||||
beforeEach(function() {
|
||||
uncaughtExceptions = [];
|
||||
});
|
||||
//global afterEach to capture uncaught exceptions
|
||||
afterEach(function() {
|
||||
assert.equal(
|
||||
uncaughtExceptions.length,
|
||||
0,
|
||||
'uncaughtException:\n\n' + uncaughtExceptions.map(err => err.stack).join('\n\n'));
|
||||
});
|
||||
|
||||
|
||||
var redisClient;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
require('../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
|
||||
|
||||
describe('error-middleware', function() {
|
||||
|
||||
it('different formats for postgis plugin error returns 400 as status code', function() {
|
||||
|
||||
var expectedStatusCode = 400;
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for single line does not match"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for multiline/PSQL does not match"
|
||||
);
|
||||
});
|
||||
});
|
||||
178
test/unit/cartodb/error-middleware.test.js
Normal file
178
test/unit/cartodb/error-middleware.test.js
Normal file
@@ -0,0 +1,178 @@
|
||||
require('../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
|
||||
|
||||
describe('error-middleware', function() {
|
||||
|
||||
it('different formats for postgis plugin error returns 400 as status code', function() {
|
||||
|
||||
var expectedStatusCode = 400;
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for single line does not match"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for multiline/PSQL does not match"
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a header with errors', function (done) {
|
||||
const error = new Error('error test');
|
||||
error.label = 'test label';
|
||||
error.type = 'test type';
|
||||
error.subtype = 'test subtype';
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
json () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('JSONP should return a header with error status code', function (done) {
|
||||
const error = new Error('error test');
|
||||
error.label = 'test label';
|
||||
error.type = 'test type';
|
||||
error.subtype = 'test subtype';
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {
|
||||
query: { callback: true }
|
||||
};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
jsonp () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should escape chars that broke logs regex', function (done) {
|
||||
const badString = 'error: ( ) = " \" \' * $ & |';
|
||||
const escapedString = 'error ';
|
||||
|
||||
const error = new Error(badString);
|
||||
error.label = badString;
|
||||
error.type = badString;
|
||||
error.subtype = badString;
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
json () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: escapedString,
|
||||
name: error.name,
|
||||
label: escapedString,
|
||||
type: escapedString,
|
||||
subtype: escapedString,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: escapedString,
|
||||
name: error.name,
|
||||
label: escapedString,
|
||||
type: escapedString,
|
||||
subtype: escapedString
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,10 @@ require('../../../support/test_helper');
|
||||
var assert = require('../../../support/assert');
|
||||
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
|
||||
|
||||
describe('ResourceLocator.getUrls', function() {
|
||||
describe('ResourceLocator', function() {
|
||||
var USERNAME = 'username';
|
||||
var RESOURCE = 'wadus';
|
||||
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
|
||||
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
|
||||
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
|
||||
|
||||
@@ -15,118 +16,292 @@ describe('ResourceLocator.getUrls', function() {
|
||||
assert.ok(urls);
|
||||
});
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('basic', function() {
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('resource templates', function() {
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn templates', function() {
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://{s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn_{s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn and resource templates', function() {
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
`https://cdn_${httpsSubdomain}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${RESOURCE}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://{s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn_{s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
@@ -84,7 +84,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
function mockStatsClientGetInstance(instance) {
|
||||
global.statsClient = instance;
|
||||
global.statsClient = Object.assign(global.statsClient, instance);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
199
yarn.lock
199
yarn.lock
@@ -2,7 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"abaculus@github:cartodb/abaculus#2.0.3-cdb1":
|
||||
abaculus@cartodb/abaculus#2.0.3-cdb1:
|
||||
version "2.0.3-cdb1"
|
||||
resolved "https://codeload.github.com/cartodb/abaculus/tar.gz/f5f34e1c80cdd8d49edd1d6fe3b2220ab2e23aaf"
|
||||
dependencies:
|
||||
@@ -10,7 +10,11 @@
|
||||
mapnik "~3.5.0"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
||||
@@ -29,13 +33,13 @@ ajv@^4.9.1:
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.1.0:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
align-text@^0.1.1, align-text@^0.1.3:
|
||||
version "0.1.4"
|
||||
@@ -211,18 +215,18 @@ camelcase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
|
||||
|
||||
camshaft@0.59.4:
|
||||
version "0.59.4"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.59.4.tgz#7d4d0b8585fa180b5e750206aa84940870ebabfc"
|
||||
camshaft@0.62.1:
|
||||
version "0.62.1"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.62.1.tgz#75f8734c4089aaeae3b9067eb94d3c669cebbcaf"
|
||||
dependencies:
|
||||
async "^1.5.2"
|
||||
bunyan "1.8.1"
|
||||
cartodb-psql "^0.10.1"
|
||||
cartodb-psql "0.11.0"
|
||||
debug "^3.1.0"
|
||||
dot "^1.0.3"
|
||||
request "^2.69.0"
|
||||
request "2.85.0"
|
||||
|
||||
"canvas@github:cartodb/node-canvas#1.6.2-cdb2":
|
||||
canvas@cartodb/node-canvas#1.6.2-cdb2:
|
||||
version "1.6.2-cdb2"
|
||||
resolved "https://codeload.github.com/cartodb/node-canvas/tar.gz/8acf04557005c633f9e68524488a2657c04f3766"
|
||||
dependencies:
|
||||
@@ -240,15 +244,15 @@ carto@0.16.3:
|
||||
semver "^5.1.0"
|
||||
yargs "^4.2.0"
|
||||
|
||||
"carto@github:cartodb/carto#0.15.1-cdb1":
|
||||
carto@CartoDB/carto#0.15.1-cdb1:
|
||||
version "0.15.1-cdb1"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
resolved "https://codeload.github.com/CartoDB/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
dependencies:
|
||||
mapnik-reference "~6.0.2"
|
||||
optimist "~0.6.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
"carto@github:cartodb/carto#0.15.1-cdb3":
|
||||
carto@cartodb/carto#0.15.1-cdb3:
|
||||
version "0.15.1-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/945f5efb74fd1af1f5e1f69f409f9567f94fb5a7"
|
||||
dependencies:
|
||||
@@ -270,6 +274,14 @@ cartodb-psql@0.10.2, cartodb-psql@^0.10.1:
|
||||
pg cartodb/node-postgres#6.1.6-cdb1
|
||||
underscore "~1.6.0"
|
||||
|
||||
cartodb-psql@0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/cartodb-psql/-/cartodb-psql-0.11.0.tgz#6b4eae0876ee56944a61fe5f4acc6a8b0b11233f"
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
pg CartoDB/node-postgres#6.4.2-cdb1
|
||||
underscore "~1.6.0"
|
||||
|
||||
cartodb-query-tables@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cartodb-query-tables/-/cartodb-query-tables-0.3.0.tgz#56e18d869666eb2e8e2cb57d0baf3acc923f8756"
|
||||
@@ -520,7 +532,11 @@ domutils@1.5:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dot@^1.0.3, dot@~1.0.2:
|
||||
dot@^1.0.3:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.1.2.tgz#c7377019fc4e550798928b2b9afeb66abfa1f2f9"
|
||||
|
||||
dot@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.0.3.tgz#f8750bfb6b03c7664eb0e6cb1eb4c66419af9427"
|
||||
|
||||
@@ -644,14 +660,22 @@ extend@~3.0.0, extend@~3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
|
||||
fast-levenshtein@~2.0.4:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
@@ -776,7 +800,7 @@ getpass@^0.1.1:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob@7.1.1, glob@^7.1.1:
|
||||
glob@7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
|
||||
dependencies:
|
||||
@@ -807,7 +831,7 @@ glob@^6.0.1:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.5:
|
||||
glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
@@ -826,9 +850,9 @@ graceful-fs@^4.1.2:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
grainstore@1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.7.0.tgz#28d78895c82e6201f7d0ff63af1056f3c0fda0d3"
|
||||
grainstore@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.0.tgz#9398729df88f3aecb55ffbb415d541dcca4420af"
|
||||
dependencies:
|
||||
carto "0.16.3"
|
||||
debug "~3.1.0"
|
||||
@@ -845,8 +869,8 @@ growl@1.9.2:
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
|
||||
|
||||
handlebars@^4.0.1:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
optimist "^0.6.1"
|
||||
@@ -998,8 +1022,8 @@ is-arrayish@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
||||
is-builtin-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1057,8 +1081,8 @@ istanbul@~0.4.3:
|
||||
wordwrap "^1.0.0"
|
||||
|
||||
js-base64@^2.1.9:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
|
||||
|
||||
js-string-escape@1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -1325,7 +1349,7 @@ mime@~1.3.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -1333,6 +1357,10 @@ minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
minimist@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.0.tgz#4dffe525dae2b864c66c2e23c6271d7afdecefce"
|
||||
@@ -1359,7 +1387,11 @@ mocha@~3.4.1:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment@^2.10.6, moment@~2.18.1:
|
||||
moment@^2.10.6:
|
||||
version "2.22.2"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
|
||||
|
||||
moment@~2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
@@ -1379,11 +1411,11 @@ mv@~2:
|
||||
ncp "~2.0.0"
|
||||
rimraf "~2.4.0"
|
||||
|
||||
nan@^2.0.8, nan@^2.3.4, nan@~2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
nan@^2.0.8:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
nan@^2.4.0:
|
||||
nan@^2.3.4, nan@^2.4.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
|
||||
|
||||
@@ -1395,6 +1427,10 @@ nan@~2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
||||
|
||||
nan@~2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
|
||||
ncp@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||
@@ -1544,6 +1580,10 @@ packet-reader@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.2.0.tgz#819df4d010b82d5ea5671f8a1a3acf039bcd7700"
|
||||
|
||||
packet-reader@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.3.1.tgz#cd62e60af8d7fea8a705ec4ff990871c46871f27"
|
||||
|
||||
parse-json@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
||||
@@ -1588,6 +1628,10 @@ pg-connection-string@0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
|
||||
pg-pool@1.*:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
|
||||
@@ -1596,9 +1640,10 @@ pg-pool@1.*:
|
||||
object-assign "4.1.0"
|
||||
|
||||
pg-types@1.*:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.12.1.tgz#d64087e3903b58ffaad279e7595c52208a14c3d2"
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
|
||||
dependencies:
|
||||
pg-int8 "1.0.1"
|
||||
postgres-array "~1.0.0"
|
||||
postgres-bytea "~1.0.0"
|
||||
postgres-date "~1.0.0"
|
||||
@@ -1617,7 +1662,20 @@ pg@cartodb/node-postgres#6.1.6-cdb1:
|
||||
pgpass "1.x"
|
||||
semver "4.3.2"
|
||||
|
||||
pgpass@1.x:
|
||||
"pg@github:CartoDB/node-postgres#6.4.2-cdb1":
|
||||
version "6.4.2"
|
||||
resolved "https://codeload.github.com/CartoDB/node-postgres/tar.gz/449fac1d6da711ffcc6694ae3c89f85244f48bdc"
|
||||
dependencies:
|
||||
buffer-writer "1.0.1"
|
||||
js-string-escape "1.0.1"
|
||||
packet-reader "0.3.1"
|
||||
pg-connection-string "0.1.3"
|
||||
pg-pool "1.*"
|
||||
pg-types "1.*"
|
||||
pgpass "1.*"
|
||||
semver "4.3.2"
|
||||
|
||||
pgpass@1.*, pgpass@1.x:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306"
|
||||
dependencies:
|
||||
@@ -1855,7 +1913,34 @@ request@2.81.0:
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.0.0"
|
||||
|
||||
request@2.x, request@^2.55.0, request@^2.69.0, request@^2.83.0:
|
||||
request@2.85.0:
|
||||
version "2.85.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.6.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.1"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.1"
|
||||
har-validator "~5.0.3"
|
||||
hawk "~6.0.2"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.17"
|
||||
oauth-sign "~0.8.2"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.1"
|
||||
safe-buffer "^5.1.1"
|
||||
stringstream "~0.0.5"
|
||||
tough-cookie "~2.3.3"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.1.0"
|
||||
|
||||
request@2.x, request@^2.55.0, request@^2.83.0:
|
||||
version "2.83.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
|
||||
dependencies:
|
||||
@@ -1917,21 +2002,17 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, s
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
safe-json-stringify@~1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
semver@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
|
||||
|
||||
semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
semver@~4.3.3:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
|
||||
@@ -1940,6 +2021,10 @@ semver@~5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
send@0.16.1:
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
|
||||
@@ -1998,8 +2083,8 @@ sntp@1.x.x:
|
||||
hoek "2.x.x"
|
||||
|
||||
sntp@2.x.x:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
|
||||
dependencies:
|
||||
hoek "4.x.x"
|
||||
|
||||
@@ -2082,7 +2167,11 @@ sshpk@^1.7.0:
|
||||
jsbn "~0.1.0"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
|
||||
"statuses@>= 1.3.1 < 2":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
statuses@~1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
||||
|
||||
@@ -2190,7 +2279,7 @@ through@2:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
|
||||
"tilelive-bridge@github:cartodb/tilelive-bridge#2.3.1-cdb4":
|
||||
tilelive-bridge@cartodb/tilelive-bridge#2.3.1-cdb4:
|
||||
version "2.3.1-cdb4"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-bridge/tar.gz/faa2b638da2d119b78281575d40255cb523f6ca6"
|
||||
dependencies:
|
||||
@@ -2198,7 +2287,7 @@ through@2:
|
||||
mapnik-pool "~0.1.3"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
"tilelive-mapnik@github:cartodb/tilelive-mapnik#0.6.18-cdb3":
|
||||
tilelive-mapnik@cartodb/tilelive-mapnik#0.6.18-cdb3:
|
||||
version "0.6.18-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-mapnik/tar.gz/23bd1c31dd57d0b76c86b9f1eaf62462b3c17d01"
|
||||
dependencies:
|
||||
@@ -2359,9 +2448,9 @@ window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
||||
windshaft@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.1.0.tgz#dc17c8369570c305171d1ab5ca130369bba04d58"
|
||||
windshaft@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.2.0.tgz#6a0409832a0d3bccfa09a88a8ab8288686b6762d"
|
||||
dependencies:
|
||||
abaculus cartodb/abaculus#2.0.3-cdb1
|
||||
canvas cartodb/node-canvas#1.6.2-cdb2
|
||||
@@ -2369,7 +2458,7 @@ windshaft@^4.1.0:
|
||||
cartodb-psql "^0.10.1"
|
||||
debug "^3.1.0"
|
||||
dot "~1.0.2"
|
||||
grainstore "1.7.0"
|
||||
grainstore "~1.8.0"
|
||||
mapnik "3.5.14"
|
||||
queue-async "~1.0.7"
|
||||
redis-mpool "0.4.1"
|
||||
|
||||
Reference in New Issue
Block a user