Windshaft-cartodb/lib/models/aggregation/aggregation-mapconfig.js

254 lines
8.0 KiB
JavaScript
Raw Normal View History

'use strict';
2017-12-19 02:06:01 +08:00
const MapConfig = require('windshaft').model.MapConfig;
const aggregationQuery = require('./aggregation-query');
const aggregationValidator = require('./aggregation-validator');
const {
2017-12-19 02:51:55 +08:00
createPositiveNumberValidator,
2017-12-19 03:42:26 +08:00
createIncludesValueValidator,
2018-03-22 00:01:32 +08:00
createAggregationColumnsValidator,
createAggregationFiltersValidator
2017-12-19 17:54:20 +08:00
} = aggregationValidator;
2017-12-19 02:06:01 +08:00
const queryUtils = require('../../utils/query-utils');
const removeDuplicates = arr => [...new Set(arr)];
2017-12-19 02:06:01 +08:00
module.exports = class AggregationMapConfig extends MapConfig {
static get AGGREGATIONS () {
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
}
2017-12-19 02:06:01 +08:00
static get PLACEMENTS () {
return aggregationQuery.SUPPORTED_PLACEMENTS;
2017-12-19 02:06:01 +08:00
}
static get THRESHOLD () {
return 1e5; // 100K
}
static get RESOLUTION () {
return 1;
}
static get SUPPORTED_GEOMETRY_TYPES () {
return [
'ST_Point'
];
}
2018-03-22 00:01:32 +08:00
static get FILTER_PARAMETERS () {
return [
// TODO: valid combinations of parameters:
// * Except for less/greater params, only one parameter allowed per filter.
// * Any less parameter can be combined with one of the greater paramters. (to define a range)
'less_than', 'less_than_or_equal_to',
'greater_than', 'greater_than_or_equal_to',
'equal', 'not_equal',
'between', 'in', 'not_in'
];
}
static get HAS_AGGREGATION_DISABLED () {
return null;
}
2019-10-22 01:07:24 +08:00
static supportsGeometryType (geometryType) {
2017-12-19 02:06:01 +08:00
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
}
2019-10-22 01:07:24 +08:00
static getAggregationGeometryColumn () {
return aggregationQuery.GEOMETRY_COLUMN;
}
constructor (user, config, connection, datasource) {
2017-12-19 02:06:01 +08:00
super(config, datasource);
2017-12-19 02:19:02 +08:00
const validate = aggregationValidator(this);
2017-12-19 03:42:26 +08:00
const positiveNumberValidator = createPositiveNumberValidator(this);
2017-12-19 02:19:02 +08:00
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
2018-03-22 00:01:32 +08:00
const aggregationFiltersValidator = createAggregationFiltersValidator(
this, AggregationMapConfig.FILTER_PARAMETERS
);
2017-12-19 02:19:02 +08:00
2017-12-19 03:42:26 +08:00
validate('resolution', positiveNumberValidator);
2017-12-19 02:19:02 +08:00
validate('placement', includesValidPlacementsValidator);
2017-12-19 03:42:26 +08:00
validate('threshold', positiveNumberValidator);
validate('columns', aggregationColumnsValidator);
2018-03-22 00:01:32 +08:00
validate('filters', aggregationFiltersValidator);
this.user = user;
this.pgConnection = connection;
2017-12-19 02:06:01 +08:00
}
getAggregatedQuery (index) {
2019-11-14 18:36:47 +08:00
const { sql_raw: sqlRaw, sql } = this.getLayer(index).options;
2017-12-19 02:06:01 +08:00
const {
// The default aggregation has no placement, columns or dimensions;
// this enables the special "full-sample" aggregation.
2017-12-19 02:06:01 +08:00
resolution = AggregationMapConfig.RESOLUTION,
threshold = AggregationMapConfig.THRESHOLD,
2017-12-27 19:48:06 +08:00
placement,
2017-12-19 02:06:01 +08:00
columns = {},
2018-03-22 00:01:32 +08:00
dimensions = {},
filters = {}
2017-12-19 02:06:01 +08:00
} = this.getAggregation(index);
return aggregationQuery({
2019-11-14 18:36:47 +08:00
query: sqlRaw || sql,
2017-12-19 02:06:01 +08:00
resolution,
threshold,
placement,
columns,
dimensions,
2018-03-22 00:01:32 +08:00
filters,
isDefaultAggregation: this._isDefaultLayerAggregation(index)
2017-12-19 02:06:01 +08:00
});
}
isAggregationLayer (index) {
2019-10-22 01:07:24 +08:00
const hasAggregation = this.hasLayerAggregation(index);
// for vector-only MapConfig are aggregated unless explicitly disabled
2019-03-07 01:32:17 +08:00
return hasAggregation || (
this.isVectorOnlyMapConfig() && hasAggregation !== AggregationMapConfig.HAS_AGGREGATION_DISABLED
);
2017-12-19 02:06:01 +08:00
}
isAggregationMapConfig () {
2017-12-19 02:06:01 +08:00
const layers = this.getLayers();
for (let index = 0; index < layers.length; index++) {
if (this.isAggregationLayer(index)) {
2017-12-19 02:06:01 +08:00
return true;
}
}
return false;
}
/* Three possible return values:
* * true: explicit aggregation ({}, true)
* * false: no aggregation (undefined)
* * AggregationMapConfig.HAS_AGGREGATION_DISABLED: explicitly disabled aggregation (false)
*/
2017-12-19 02:06:01 +08:00
hasLayerAggregation (index) {
const layer = this.getLayer(index);
const { aggregation } = layer.options;
if (aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean')) {
if (aggregation === false) {
return AggregationMapConfig.HAS_AGGREGATION_DISABLED;
}
return true;
}
return false;
2017-12-19 02:06:01 +08:00
}
getAggregation (index) {
const { aggregation } = this.getLayer(index).options;
if (this.isAggregationLayer(index)) {
if (typeof aggregation === 'boolean' || !aggregation) {
return {};
}
2017-12-19 02:06:01 +08:00
}
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);
2019-10-22 01:07:24 +08:00
const finalColumns = ['cartodb_id', '_cdb_feature_count'];
let aggregatedColumns = [];
if (columns) {
aggregatedColumns = Object.keys(columns);
}
let dimensionsColumns = [];
if (dimensions) {
dimensionsColumns = Object.keys(dimensions);
}
return removeDuplicates(finalColumns.concat(aggregatedColumns).concat(dimensionsColumns));
}
2019-10-22 01:07:24 +08:00
doesLayerReachThreshold (index, featureCount) {
const threshold = this.getAggregation(index) && this.getAggregation(index).threshold
? this.getAggregation(index).threshold
: AggregationMapConfig.THRESHOLD;
2017-12-19 02:06:01 +08:00
return featureCount >= threshold;
}
getLayerColumns (index, skipGeoms, callback) {
const geomColumns = ['the_geom', 'the_geom_webmercator'];
2019-06-27 00:03:02 +08:00
const limitedQuery = ctx => `SELECT * FROM (${ctx.query}) __cdb_aggregation_schema LIMIT 0`;
const layer = this.getLayer(index);
this.pgConnection.getConnection(this.user, (err, connection) => {
if (err) {
return callback(err);
}
const sql = limitedQuery({
query: queryUtils.substituteDummyTokens(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 aggregation && this._isDefaultAggregation(aggregation);
}
2017-12-30 01:25:08 +08:00
_isDefaultAggregation (aggregation) {
return aggregation.placement === undefined &&
aggregation.columns === undefined &&
2018-03-22 00:01:32 +08:00
this._isEmptyParameter(aggregation.dimensions) &&
this._isEmptyParameter(aggregation.filters);
}
2019-10-22 01:07:24 +08:00
_isEmptyParameter (parameter) {
return parameter === undefined || parameter === null || this._isEmptyObject(parameter);
}
_isEmptyObject (parameter) {
return typeof parameter === 'object' && Object.keys(parameter).length === 0;
}
2017-12-19 02:06:01 +08:00
};