Initial checkin for dataviews
It only supports histograms.
This commit is contained in:
parent
697749b204
commit
b3bbb9d97a
167
lib/cartodb/backends/dataview.js
Normal file
167
lib/cartodb/backends/dataview.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
var _ = require('underscore');
|
||||||
|
var PSQL = require('cartodb-psql');
|
||||||
|
var camshaft = require('camshaft');
|
||||||
|
var step = require('step');
|
||||||
|
|
||||||
|
var Timer = require('../stats/timer');
|
||||||
|
|
||||||
|
var BBoxFilter = require('../models/filter/bbox');
|
||||||
|
var Histogram = require('../models/dataview/histogram');
|
||||||
|
|
||||||
|
function DataviewBackend() {
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DataviewBackend;
|
||||||
|
|
||||||
|
|
||||||
|
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
|
||||||
|
var timer = new Timer();
|
||||||
|
|
||||||
|
var mapConfig;
|
||||||
|
var dataviewDefinition;
|
||||||
|
step(
|
||||||
|
function getMapConfig() {
|
||||||
|
mapConfigProvider.getMapConfig(this);
|
||||||
|
},
|
||||||
|
function getWidget(err, _mapConfig) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
mapConfig = _mapConfig;
|
||||||
|
|
||||||
|
var _dataviewDefinition = getDataviewDefinition(mapConfig.obj(), params.dataviewName);
|
||||||
|
if (!_dataviewDefinition) {
|
||||||
|
throw new Error("Dataview '" + params.dataviewName + "' does not exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
dataviewDefinition = _dataviewDefinition;
|
||||||
|
|
||||||
|
return dataviewDefinition;
|
||||||
|
},
|
||||||
|
function loadAnalysis(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
var analysisConfiguration = {
|
||||||
|
db: {
|
||||||
|
host: params.dbhost,
|
||||||
|
port: params.dbport,
|
||||||
|
dbname: params.dbname,
|
||||||
|
user: params.dbuser,
|
||||||
|
pass: params.dbpassword
|
||||||
|
},
|
||||||
|
batch: {
|
||||||
|
// TODO load this from configuration
|
||||||
|
endpoint: 'http://127.0.0.1:8080/api/v1/sql/job',
|
||||||
|
username: user,
|
||||||
|
apiKey: params.api_key
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sourceId = dataviewDefinition.source.id;
|
||||||
|
var analysisDefinition = getAnalysisDefinition(mapConfig.obj().analyses, sourceId);
|
||||||
|
|
||||||
|
var next = this;
|
||||||
|
|
||||||
|
camshaft.create(analysisConfiguration, analysisDefinition, function(err, analysis) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceId2Node = {};
|
||||||
|
var rootNode = analysis.getRoot();
|
||||||
|
if (rootNode.params && rootNode.params.id) {
|
||||||
|
sourceId2Node[rootNode.params.id] = rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.getSortedNodes().forEach(function(node) {
|
||||||
|
if (node.params && node.params.id) {
|
||||||
|
sourceId2Node[node.params.id] = node;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var node = sourceId2Node[sourceId];
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return next(new Error('Analysis node not found for dataview'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(null, node);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function runDataviewQuery(err, node) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||||
|
|
||||||
|
var ownFilter = +params.own_filter;
|
||||||
|
ownFilter = !!ownFilter;
|
||||||
|
|
||||||
|
var query;
|
||||||
|
if (ownFilter) {
|
||||||
|
query = node.getQuery();
|
||||||
|
} else {
|
||||||
|
var applyFilters = {};
|
||||||
|
applyFilters[params.dataviewName] = false;
|
||||||
|
query = node.getQuery(applyFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.bbox) {
|
||||||
|
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox});
|
||||||
|
query = bboxFilter.sql(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||||
|
function castNumbers(overrides, val, k) {
|
||||||
|
overrides[k] = Number.isFinite(+val) ? +val : val;
|
||||||
|
return overrides;
|
||||||
|
},
|
||||||
|
{ownFilter: ownFilter}
|
||||||
|
);
|
||||||
|
|
||||||
|
var histogramDataview = new Histogram(query, dataviewDefinition.options);
|
||||||
|
histogramDataview.getResult(pg, overrideParams, this);
|
||||||
|
},
|
||||||
|
function returnCallback(err, result) {
|
||||||
|
return callback(err, result, timer.getTimes());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAnalysisDefinition(mapConfigAnalyses, sourceId) {
|
||||||
|
mapConfigAnalyses = mapConfigAnalyses || [];
|
||||||
|
for (var i = 0; i < mapConfigAnalyses.length; i++) {
|
||||||
|
var analysisGraph = new camshaft.reference.AnalysisGraph(mapConfigAnalyses[i]);
|
||||||
|
var nodes = analysisGraph.getNodesWithId();
|
||||||
|
if (nodes.hasOwnProperty(sourceId)) {
|
||||||
|
return mapConfigAnalyses[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('There is no associated analysis for the dataview source id');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataviewDefinition(mapConfig, dataviewName) {
|
||||||
|
var dataviews = mapConfig.dataviews || {};
|
||||||
|
return dataviews[dataviewName];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbParamsFromReqParams(params) {
|
||||||
|
var dbParams = {};
|
||||||
|
if ( params.dbuser ) {
|
||||||
|
dbParams.user = params.dbuser;
|
||||||
|
}
|
||||||
|
if ( params.dbpassword ) {
|
||||||
|
dbParams.pass = params.dbpassword;
|
||||||
|
}
|
||||||
|
if ( params.dbhost ) {
|
||||||
|
dbParams.host = params.dbhost;
|
||||||
|
}
|
||||||
|
if ( params.dbport ) {
|
||||||
|
dbParams.port = params.dbport;
|
||||||
|
}
|
||||||
|
if ( params.dbname ) {
|
||||||
|
dbParams.dbname = params.dbname;
|
||||||
|
}
|
||||||
|
return dbParams;
|
||||||
|
}
|
@ -7,6 +7,8 @@ var BaseController = require('./base');
|
|||||||
var cors = require('../middleware/cors');
|
var cors = require('../middleware/cors');
|
||||||
var userMiddleware = require('../middleware/user');
|
var userMiddleware = require('../middleware/user');
|
||||||
|
|
||||||
|
var DataviewBackend = require('../backends/dataview');
|
||||||
|
|
||||||
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
|
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
|
||||||
|
|
||||||
var QueryTables = require('cartodb-query-tables');
|
var QueryTables = require('cartodb-query-tables');
|
||||||
@ -37,6 +39,8 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
|
|||||||
this.surrogateKeysCache = surrogateKeysCache;
|
this.surrogateKeysCache = surrogateKeysCache;
|
||||||
this.userLimitsApi = userLimitsApi;
|
this.userLimitsApi = userLimitsApi;
|
||||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||||
|
|
||||||
|
this.dataviewBackend = new DataviewBackend();
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(LayergroupController, BaseController);
|
util.inherits(LayergroupController, BaseController);
|
||||||
@ -78,6 +82,38 @@ LayergroupController.prototype.register = function(app) {
|
|||||||
app.get(app.base_url_mapconfig +
|
app.get(app.base_url_mapconfig +
|
||||||
'/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
|
'/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
|
||||||
this.widgetSearch.bind(this));
|
this.widgetSearch.bind(this));
|
||||||
|
|
||||||
|
app.get(app.base_url_mapconfig +
|
||||||
|
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
||||||
|
this.dataview.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
LayergroupController.prototype.dataview = function(req, res) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
step(
|
||||||
|
function setupParams() {
|
||||||
|
self.req2params(req, this);
|
||||||
|
},
|
||||||
|
function retrieveList(err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||||
|
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||||
|
);
|
||||||
|
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
||||||
|
},
|
||||||
|
function finish(err, tile, stats) {
|
||||||
|
req.profiler.add(stats || {});
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
self.sendError(req, res, err, 'GET WIDGET');
|
||||||
|
} else {
|
||||||
|
self.sendResponse(req, res, tile, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LayergroupController.prototype.widget = function(req, res) {
|
LayergroupController.prototype.widget = function(req, res) {
|
||||||
|
@ -155,7 +155,16 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
|||||||
apiKey: req.params.api_key
|
apiKey: req.params.api_key
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.analysisMapConfigAdapter.getLayers(analysisConfiguration, requestMapConfig, this);
|
|
||||||
|
var filters = {};
|
||||||
|
if (req.params.filters) {
|
||||||
|
try {
|
||||||
|
filters = JSON.parse(req.params.filters);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.analysisMapConfigAdapter.getLayers(analysisConfiguration, requestMapConfig, filters, this);
|
||||||
},
|
},
|
||||||
function beforeLayergroupCreate(err, requestMapConfig) {
|
function beforeLayergroupCreate(err, requestMapConfig) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
|
@ -31,19 +31,13 @@ function layerQuery(query, columnNames) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replaceSourceRootQueries(requestMapConfig) {
|
function replaceSourceRootQueries(requestMapConfig) {
|
||||||
var analysisToRemove = {};
|
var analysisSourceRootsIds = requestMapConfig.analyses.reduce(function(sourceRootsIds, analysis) {
|
||||||
var analysisSourceRootsIds = requestMapConfig.analyses.reduce(function(sourceRootsIds, analysis, analysisIndex) {
|
|
||||||
if (analysis.type === 'source' && !!analysis.id) {
|
if (analysis.type === 'source' && !!analysis.id) {
|
||||||
sourceRootsIds[analysis.id] = analysis;
|
sourceRootsIds[analysis.id] = analysis;
|
||||||
analysisToRemove[analysisIndex] = true;
|
|
||||||
}
|
}
|
||||||
return sourceRootsIds;
|
return sourceRootsIds;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
requestMapConfig.analyses = requestMapConfig.analyses.filter(function(analysis, index) {
|
|
||||||
return !analysisToRemove.hasOwnProperty(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
requestMapConfig.layers = requestMapConfig.layers.map(function(layer) {
|
requestMapConfig.layers = requestMapConfig.layers.map(function(layer) {
|
||||||
var sourceId = layer.options && layer.options.source && layer.options.source.id;
|
var sourceId = layer.options && layer.options.source && layer.options.source.id;
|
||||||
if (sourceId && analysisSourceRootsIds.hasOwnProperty(sourceId)) {
|
if (sourceId && analysisSourceRootsIds.hasOwnProperty(sourceId)) {
|
||||||
@ -56,17 +50,61 @@ function replaceSourceRootQueries(requestMapConfig) {
|
|||||||
return requestMapConfig;
|
return requestMapConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||||
|
var analyses = requestMapConfig.analyses || [];
|
||||||
|
|
||||||
|
debug(JSON.stringify(requestMapConfig, null, 4));
|
||||||
|
|
||||||
|
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
|
||||||
|
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);
|
||||||
|
var definition = analysisDefinition;
|
||||||
|
Object.keys(dataviewsFiltersBySourceId).forEach(function(sourceId) {
|
||||||
|
definition = analysisGraph.getDefinitionWith(sourceId, {filters: dataviewsFiltersBySourceId[sourceId] });
|
||||||
|
});
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(JSON.stringify(requestMapConfig, null, 4));
|
||||||
|
|
||||||
|
return requestMapConfig;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldAdaptLayers(requestMapConfig) {
|
function shouldAdaptLayers(requestMapConfig) {
|
||||||
return Array.isArray(requestMapConfig.layers) &&
|
return Array.isArray(requestMapConfig.layers) &&
|
||||||
Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0;
|
Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalysisMapConfigAdapter.prototype.getLayers = function(analysisConfiguration, requestMapConfig, callback) {
|
AnalysisMapConfigAdapter.prototype.getLayers = function(analysisConfiguration, requestMapConfig, filters, callback) {
|
||||||
|
filters = filters || {};
|
||||||
|
var dataviewsFilters = filters.dataviews || {};
|
||||||
|
debug(dataviewsFilters);
|
||||||
|
var dataviews = requestMapConfig.dataviews || {};
|
||||||
|
|
||||||
|
var dataviewsFiltersBySourceId = Object.keys(dataviewsFilters).reduce(function(bySourceId, dataviewName) {
|
||||||
|
var dataview = dataviews[dataviewName];
|
||||||
|
if (dataview) {
|
||||||
|
var sourceId = dataview.source.id;
|
||||||
|
if (!bySourceId.hasOwnProperty(sourceId)) {
|
||||||
|
bySourceId[sourceId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bySourceId[sourceId][dataviewName] = {
|
||||||
|
type: 'range',
|
||||||
|
column: dataview.options.column,
|
||||||
|
params: dataviewsFilters[dataviewName]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return bySourceId;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
debug(dataviewsFiltersBySourceId);
|
||||||
|
|
||||||
debug('mapconfig input', JSON.stringify(requestMapConfig, null, 4));
|
debug('mapconfig input', JSON.stringify(requestMapConfig, null, 4));
|
||||||
|
|
||||||
if (Array.isArray(requestMapConfig.analyses)) {
|
if (Array.isArray(requestMapConfig.analyses)) {
|
||||||
requestMapConfig = replaceSourceRootQueries(requestMapConfig);
|
requestMapConfig = replaceSourceRootQueries(requestMapConfig);
|
||||||
|
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldAdaptLayers(requestMapConfig)) {
|
if (!shouldAdaptLayers(requestMapConfig)) {
|
||||||
|
318
lib/cartodb/models/dataview/histogram.js
Normal file
318
lib/cartodb/models/dataview/histogram.js
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
var _ = require('underscore');
|
||||||
|
var debug = require('debug')('windshaft:dataview:histogram');
|
||||||
|
|
||||||
|
var dot = require('dot');
|
||||||
|
dot.templateSettings.strip = false;
|
||||||
|
|
||||||
|
var columnTypeQueryTpl = dot.template(
|
||||||
|
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||||
|
);
|
||||||
|
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||||
|
|
||||||
|
var BIN_MIN_NUMBER = 6;
|
||||||
|
var BIN_MAX_NUMBER = 48;
|
||||||
|
|
||||||
|
var basicsQueryTpl = dot.template([
|
||||||
|
'basics AS (',
|
||||||
|
' SELECT',
|
||||||
|
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||||
|
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||||
|
' FROM ({{=it._query}}) _cdb_basics',
|
||||||
|
')'
|
||||||
|
].join(' \n'));
|
||||||
|
|
||||||
|
var overrideBasicsQueryTpl = dot.template([
|
||||||
|
'basics AS (',
|
||||||
|
' SELECT',
|
||||||
|
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||||
|
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||||
|
' FROM ({{=it._query}}) _cdb_basics',
|
||||||
|
')'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var iqrQueryTpl = dot.template([
|
||||||
|
'iqrange AS (',
|
||||||
|
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||||
|
' FROM (',
|
||||||
|
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||||
|
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||||
|
' ) AS quartile',
|
||||||
|
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||||
|
' WHERE quartile = 1 or quartile = 3',
|
||||||
|
' GROUP BY quartile',
|
||||||
|
' ) _cdb_iqr',
|
||||||
|
')'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var binsQueryTpl = dot.template([
|
||||||
|
'bins AS (',
|
||||||
|
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||||
|
' THEN 1',
|
||||||
|
' ELSE GREATEST(',
|
||||||
|
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||||
|
' LEAST(',
|
||||||
|
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||||
|
' {{=it._maxBins}}',
|
||||||
|
' )',
|
||||||
|
' )',
|
||||||
|
' END AS bins_number',
|
||||||
|
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||||
|
' LIMIT 1',
|
||||||
|
')'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var overrideBinsQueryTpl = dot.template([
|
||||||
|
'bins AS (',
|
||||||
|
' SELECT {{=it._bins}} AS bins_number',
|
||||||
|
')'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var nullsQueryTpl = dot.template([
|
||||||
|
'nulls AS (',
|
||||||
|
' SELECT',
|
||||||
|
' count(*) AS nulls_count',
|
||||||
|
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||||
|
' WHERE {{=it._column}} IS NULL',
|
||||||
|
')'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var histogramQueryTpl = dot.template([
|
||||||
|
'SELECT',
|
||||||
|
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||||
|
' bins_number,',
|
||||||
|
' nulls_count,',
|
||||||
|
' avg_val,',
|
||||||
|
' CASE WHEN min_val = max_val',
|
||||||
|
' THEN 0',
|
||||||
|
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||||
|
' END AS bin,',
|
||||||
|
' min({{=it._column}})::numeric AS min,',
|
||||||
|
' max({{=it._column}})::numeric AS max,',
|
||||||
|
' avg({{=it._column}})::numeric AS avg,',
|
||||||
|
' count(*) AS freq',
|
||||||
|
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||||
|
'WHERE {{=it._column}} IS NOT NULL',
|
||||||
|
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||||
|
'ORDER BY bin'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
|
||||||
|
var TYPE = 'histogram';
|
||||||
|
|
||||||
|
/**
|
||||||
|
{
|
||||||
|
type: 'histogram',
|
||||||
|
options: {
|
||||||
|
column: 'name',
|
||||||
|
bins: 10 // OPTIONAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function Histogram(query, options) {
|
||||||
|
if (!_.isString(options.column)) {
|
||||||
|
throw new Error('Histogram expects `column` in widget options');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.query = query;
|
||||||
|
this.column = options.column;
|
||||||
|
this.bins = options.bins;
|
||||||
|
|
||||||
|
this._columnType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Histogram;
|
||||||
|
|
||||||
|
var DATE_OIDS = {
|
||||||
|
1082: true,
|
||||||
|
1114: true,
|
||||||
|
1184: true
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Histogram.prototype.getResult = function(psql, override, callback) {
|
||||||
|
var self = this;
|
||||||
|
this.sql(psql, override, function(err, query) {
|
||||||
|
psql.query(query, function(err, result) {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return callback(err, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.format(result, override);
|
||||||
|
result.type = self.getType();
|
||||||
|
|
||||||
|
return callback(null, result);
|
||||||
|
|
||||||
|
}, true); // use read-only transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
Histogram.prototype.search = function(psql, filters, userQuery, callback) {
|
||||||
|
return callback(null, this.format({ rows: [] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Histogram.prototype.sql = function(psql, override, callback) {
|
||||||
|
if (!callback) {
|
||||||
|
callback = override;
|
||||||
|
override = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var _column = this.column;
|
||||||
|
|
||||||
|
var columnTypeQuery = columnTypeQueryTpl({
|
||||||
|
column: _column, query: this.query
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this._columnType === null) {
|
||||||
|
psql.query(columnTypeQuery, function(err, result) {
|
||||||
|
// assume numeric, will fail later
|
||||||
|
self._columnType = 'numeric';
|
||||||
|
if (!err && !!result.rows[0]) {
|
||||||
|
var pgType = result.rows[0].pg_typeof;
|
||||||
|
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||||
|
self._columnType = 'date';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sql(psql, override, callback);
|
||||||
|
}, true); // use read-only transaction
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._columnType === 'date') {
|
||||||
|
_column = columnCastTpl({column: _column});
|
||||||
|
}
|
||||||
|
|
||||||
|
var _query = this.query;
|
||||||
|
|
||||||
|
var basicsQuery, binsQuery;
|
||||||
|
|
||||||
|
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||||
|
debug('overriding with %j', override);
|
||||||
|
basicsQuery = overrideBasicsQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_column: _column,
|
||||||
|
_start: override.start,
|
||||||
|
_end: override.end
|
||||||
|
});
|
||||||
|
|
||||||
|
binsQuery = [
|
||||||
|
overrideBinsQueryTpl({
|
||||||
|
_bins: override.bins
|
||||||
|
})
|
||||||
|
].join(',\n');
|
||||||
|
} else {
|
||||||
|
basicsQuery = basicsQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_column: _column
|
||||||
|
});
|
||||||
|
|
||||||
|
if (override && _.has(override, 'bins')) {
|
||||||
|
binsQuery = [
|
||||||
|
overrideBinsQueryTpl({
|
||||||
|
_bins: override.bins
|
||||||
|
})
|
||||||
|
].join(',\n');
|
||||||
|
} else {
|
||||||
|
binsQuery = [
|
||||||
|
iqrQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_column: _column
|
||||||
|
}),
|
||||||
|
binsQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_minBins: BIN_MIN_NUMBER,
|
||||||
|
_maxBins: BIN_MAX_NUMBER
|
||||||
|
})
|
||||||
|
].join(',\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var histogramSql = [
|
||||||
|
"WITH",
|
||||||
|
[
|
||||||
|
basicsQuery,
|
||||||
|
binsQuery,
|
||||||
|
nullsQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_column: _column
|
||||||
|
})
|
||||||
|
].join(',\n'),
|
||||||
|
histogramQueryTpl({
|
||||||
|
_query: _query,
|
||||||
|
_column: _column
|
||||||
|
})
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
debug(histogramSql);
|
||||||
|
|
||||||
|
return callback(null, histogramSql);
|
||||||
|
};
|
||||||
|
|
||||||
|
Histogram.prototype.format = function(result, override) {
|
||||||
|
override = override || {};
|
||||||
|
var buckets = [];
|
||||||
|
|
||||||
|
var binsCount = getBinsCount(override);
|
||||||
|
var width = getWidth(override);
|
||||||
|
var binsStart = getBinStart(override);
|
||||||
|
var nulls = 0;
|
||||||
|
var avg;
|
||||||
|
|
||||||
|
if (result.rows.length) {
|
||||||
|
var firstRow = result.rows[0];
|
||||||
|
binsCount = firstRow.bins_number;
|
||||||
|
width = firstRow.bin_width || width;
|
||||||
|
avg = firstRow.avg_val;
|
||||||
|
nulls = firstRow.nulls_count;
|
||||||
|
binsStart = override.hasOwnProperty('start') ? override.start : firstRow.min;
|
||||||
|
|
||||||
|
buckets = result.rows.map(function(row) {
|
||||||
|
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bin_width: width,
|
||||||
|
bins_count: binsCount,
|
||||||
|
bins_start: binsStart,
|
||||||
|
nulls: nulls,
|
||||||
|
avg: avg,
|
||||||
|
bins: buckets
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBinStart(override) {
|
||||||
|
return override.start || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBinsCount(override) {
|
||||||
|
return override.bins || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWidth(override) {
|
||||||
|
var width = 0;
|
||||||
|
var binsCount = override.bins;
|
||||||
|
|
||||||
|
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
|
||||||
|
width = (override.end - override.start) / binsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
Histogram.prototype.getType = function() {
|
||||||
|
return TYPE;
|
||||||
|
};
|
||||||
|
|
||||||
|
Histogram.prototype.toString = function() {
|
||||||
|
return JSON.stringify({
|
||||||
|
_type: TYPE,
|
||||||
|
_column: this.column,
|
||||||
|
_query: this.query
|
||||||
|
});
|
||||||
|
};
|
120
lib/cartodb/models/filter/bbox.js
Normal file
120
lib/cartodb/models/filter/bbox.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
var debug = require('debug')('windshaft:filter:bbox');
|
||||||
|
var dot = require('dot');
|
||||||
|
dot.templateSettings.strip = false;
|
||||||
|
|
||||||
|
var filterQueryTpl = dot.template([
|
||||||
|
'SELECT * FROM ({{=it._sql}}) _cdb_bbox_filter',
|
||||||
|
'WHERE {{=it._filters}}'
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
var bboxFilterTpl = '{{=it._column}} && ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}})';
|
||||||
|
|
||||||
|
var LATITUDE_MAX_VALUE = 85.0511287798066;
|
||||||
|
var LONGITUDE_LOWER_BOUND = -180;
|
||||||
|
var LONGITUDE_UPPER_BOUND = 180;
|
||||||
|
var LONGITUDE_RANGE = LONGITUDE_UPPER_BOUND - LONGITUDE_LOWER_BOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Definition
|
||||||
|
{
|
||||||
|
"type”: "bbox",
|
||||||
|
"options": {
|
||||||
|
"column": "the_geom_webmercator",
|
||||||
|
"srid": 3857
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Params
|
||||||
|
{
|
||||||
|
“bbox”: "west,south,east,north"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function BBox(filterDefinition, filterParams) {
|
||||||
|
var bbox = filterParams.bbox;
|
||||||
|
|
||||||
|
if (!bbox) {
|
||||||
|
throw new Error('BBox filter expects to have a bbox param');
|
||||||
|
}
|
||||||
|
|
||||||
|
var bboxElements = bbox.split(',').map(function(e) { return +e; });
|
||||||
|
|
||||||
|
validateBboxElements(bboxElements);
|
||||||
|
|
||||||
|
this.column = filterDefinition.column || 'the_geom_webmercator';
|
||||||
|
this.srid = filterDefinition.srid || 3857;
|
||||||
|
|
||||||
|
// Latitudes must be within max extent
|
||||||
|
var south = Math.max(bboxElements[1], -LATITUDE_MAX_VALUE);
|
||||||
|
var north = Math.min(bboxElements[3], LATITUDE_MAX_VALUE);
|
||||||
|
|
||||||
|
// Longitudes crossing 180º need another approach
|
||||||
|
var adjustedLongitudeRange = adjustLongitudeRange([bboxElements[0], bboxElements[2]]);
|
||||||
|
var west = adjustedLongitudeRange[0];
|
||||||
|
var east = adjustedLongitudeRange[1];
|
||||||
|
|
||||||
|
this.bboxes = getBoundingBoxes(west, south, east, north);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoundingBoxes(west, south, east, north) {
|
||||||
|
var bboxes = [];
|
||||||
|
|
||||||
|
if (east - west >= 360) {
|
||||||
|
bboxes.push([-180, south, 180, north]);
|
||||||
|
} else if (west >= -180 && east <= 180) {
|
||||||
|
bboxes.push([west, south, east, north]);
|
||||||
|
} else {
|
||||||
|
bboxes.push([west, south, 180, north]);
|
||||||
|
bboxes.push([-180, south, east % 180, north]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bboxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBboxElements(bboxElements) {
|
||||||
|
var isNumericBbox = bboxElements
|
||||||
|
.map(function(n) { return Number.isFinite(n); })
|
||||||
|
.reduce(function(allFinite, isFinite) {
|
||||||
|
if (!allFinite) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isFinite;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
if (bboxElements.length !== 4 || !isNumericBbox) {
|
||||||
|
throw new Error('Invalid bbox filter, expected format="west,south,east,north"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustLongitudeRange(we) {
|
||||||
|
var west = we[0];
|
||||||
|
west -= LONGITUDE_LOWER_BOUND;
|
||||||
|
west = west - (LONGITUDE_RANGE * Math.floor(west / LONGITUDE_RANGE)) + LONGITUDE_LOWER_BOUND;
|
||||||
|
|
||||||
|
var longitudeRange = Math.min(we[1] - we[0], 360);
|
||||||
|
|
||||||
|
return [west, west + longitudeRange];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BBox;
|
||||||
|
|
||||||
|
module.exports.adjustLongitudeRange = adjustLongitudeRange;
|
||||||
|
module.exports.LATITUDE_MAX_VALUE = LATITUDE_MAX_VALUE;
|
||||||
|
module.exports.LONGITUDE_MAX_VALUE = LONGITUDE_UPPER_BOUND;
|
||||||
|
|
||||||
|
|
||||||
|
BBox.prototype.sql = function(rawSql) {
|
||||||
|
var bboxSql = filterQueryTpl({
|
||||||
|
_sql: rawSql,
|
||||||
|
_filters: this.bboxes.map(function(bbox) {
|
||||||
|
return bboxFilterTpl({
|
||||||
|
_column: this.column,
|
||||||
|
_bbox: bbox.join(','),
|
||||||
|
_srid: this.srid
|
||||||
|
});
|
||||||
|
}.bind(this)).join(' OR ')
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(bboxSql);
|
||||||
|
|
||||||
|
return bboxSql;
|
||||||
|
};
|
124
test/acceptance/analysis/analysis-layers-dataviews.js
Normal file
124
test/acceptance/analysis/analysis-layers-dataviews.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
require('../../support/test_helper');
|
||||||
|
|
||||||
|
var assert = require('../../support/assert');
|
||||||
|
var TestClient = require('../../support/test-client');
|
||||||
|
var dot = require('dot');
|
||||||
|
|
||||||
|
describe('analysis-layers-dataviews', function() {
|
||||||
|
|
||||||
|
var multitypeStyleTemplate = dot.template([
|
||||||
|
"#points['mapnik::geometry_type'=1] {",
|
||||||
|
" marker-fill-opacity: {{=it._opacity}};",
|
||||||
|
" marker-line-color: #FFF;",
|
||||||
|
" marker-line-width: 0.5;",
|
||||||
|
" marker-line-opacity: {{=it._opacity}};",
|
||||||
|
" marker-placement: point;",
|
||||||
|
" marker-type: ellipse;",
|
||||||
|
" marker-width: 8;",
|
||||||
|
" marker-fill: {{=it._color}};",
|
||||||
|
" marker-allow-overlap: true;",
|
||||||
|
"}",
|
||||||
|
"#lines['mapnik::geometry_type'=2] {",
|
||||||
|
" line-color: {{=it._color}};",
|
||||||
|
" line-width: 2;",
|
||||||
|
" line-opacity: {{=it._opacity}};",
|
||||||
|
"}",
|
||||||
|
"#polygons['mapnik::geometry_type'=3] {",
|
||||||
|
" polygon-fill: {{=it._color}};",
|
||||||
|
" polygon-opacity: {{=it._opacity}};",
|
||||||
|
" line-color: #FFF;",
|
||||||
|
" line-width: 0.5;",
|
||||||
|
" line-opacity: {{=it._opacity}};",
|
||||||
|
"}"
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
|
||||||
|
function cartocss(color, opacity) {
|
||||||
|
return multitypeStyleTemplate({
|
||||||
|
_color: color || '#F11810',
|
||||||
|
_opacity: Number.isFinite(opacity) ? opacity : 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapConfig(layers, dataviews, analysis) {
|
||||||
|
return {
|
||||||
|
version: '1.5.0',
|
||||||
|
layers: layers,
|
||||||
|
dataviews: dataviews || {},
|
||||||
|
analyses: analysis || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||||
|
|
||||||
|
var mapConfig = createMapConfig(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "cartodb",
|
||||||
|
"options": {
|
||||||
|
"source": {
|
||||||
|
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||||
|
},
|
||||||
|
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||||
|
"cartocss_version": "2.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
pop_max_histogram: {
|
||||||
|
source: {
|
||||||
|
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||||
|
},
|
||||||
|
type: 'histogram',
|
||||||
|
options: {
|
||||||
|
column: 'pop_max'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||||
|
"type": "source",
|
||||||
|
"params": {
|
||||||
|
"query": "select * from populated_places_simple_reduced"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should get histogram dataview', function(done) {
|
||||||
|
var testClient = new TestClient(mapConfig, 1234);
|
||||||
|
|
||||||
|
testClient.getDataview('pop_max_histogram', function(err, dataview) {
|
||||||
|
assert.ok(!err, err);
|
||||||
|
|
||||||
|
assert.equal(dataview.type, 'histogram');
|
||||||
|
assert.equal(dataview.bins_start, 0);
|
||||||
|
|
||||||
|
testClient.drain(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a filtered histogram dataview', function(done) {
|
||||||
|
var testClient = new TestClient(mapConfig, 1234);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
filters: {
|
||||||
|
dataviews: {
|
||||||
|
pop_max_histogram: {
|
||||||
|
min: 2e6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||||
|
assert.ok(!err, err);
|
||||||
|
|
||||||
|
assert.equal(dataview.type, 'histogram');
|
||||||
|
assert.equal(dataview.bins_start, 2008000);
|
||||||
|
|
||||||
|
testClient.drain(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -117,6 +117,113 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
callback = params;
|
||||||
|
params = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var extraParams = {};
|
||||||
|
if (this.apiKey) {
|
||||||
|
extraParams.api_key = this.apiKey;
|
||||||
|
}
|
||||||
|
if (params && params.filters) {
|
||||||
|
extraParams.filters = JSON.stringify(params.filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = '/api/v1/map';
|
||||||
|
if (Object.keys(extraParams).length > 0) {
|
||||||
|
url += '?' + qs.stringify(extraParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
var layergroupId;
|
||||||
|
step(
|
||||||
|
function createLayergroup() {
|
||||||
|
var next = this;
|
||||||
|
assert.response(server,
|
||||||
|
{
|
||||||
|
url: url,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
host: 'localhost',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
data: JSON.stringify(self.mapConfig)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(res, err) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
var parsedBody = JSON.parse(res.body);
|
||||||
|
// var expectedWidgetURLS = {
|
||||||
|
// http: "/api/v1/map/" + parsedBody.layergroupid + "/dataview/" + dataviewName
|
||||||
|
// };
|
||||||
|
// assert.ok(parsedBody.metadata.dataviews[dataviewName]);
|
||||||
|
// assert.ok(
|
||||||
|
// parsedBody.metadata.dataviews[dataviewName].url.http.match(expectedWidgetURLS.http)
|
||||||
|
// );
|
||||||
|
return next(null, parsedBody.layergroupid);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function getDataviewResult(err, _layergroupId) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
var next = this;
|
||||||
|
layergroupId = _layergroupId;
|
||||||
|
|
||||||
|
var urlParams = {
|
||||||
|
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
|
||||||
|
};
|
||||||
|
if (params && params.bbox) {
|
||||||
|
urlParams.bbox = params.bbox;
|
||||||
|
}
|
||||||
|
if (self.apiKey) {
|
||||||
|
urlParams.api_key = self.apiKey;
|
||||||
|
}
|
||||||
|
url = '/api/v1/map/' + layergroupId + '/dataview/' + dataviewName + '?' + qs.stringify(urlParams);
|
||||||
|
|
||||||
|
assert.response(server,
|
||||||
|
{
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'localhost'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(res, err) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
next(null, JSON.parse(res.body));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function finish(err, res) {
|
||||||
|
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||||
|
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||||
|
return callback(err, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user