diff --git a/lib/api/map/dataview-layergroup-controller.js b/lib/api/map/dataview-layergroup-controller.js index a10fbc25..446bf8a3 100644 --- a/lib/api/map/dataview-layergroup-controller.js +++ b/lib/api/map/dataview-layergroup-controller.js @@ -18,6 +18,7 @@ const ALLOWED_DATAVIEW_QUERY_PARAMS = [ 'own_filter', // 0, 1 'no_filters', // 0, 1 'bbox', // w,s,e,n + 'circle', // json 'start', // number 'end', // number 'column_type', // string diff --git a/lib/backends/dataview.js b/lib/backends/dataview.js index 080718e1..555f8e79 100644 --- a/lib/backends/dataview.js +++ b/lib/backends/dataview.js @@ -3,6 +3,7 @@ var _ = require('underscore'); var PSQL = require('cartodb-psql'); var BBoxFilter = require('../models/filter/bbox'); +const CircleFilter = require('../models/filter/circle'); var DataviewFactory = require('../models/dataview/factory'); var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory'); const dbParamsFromReqParams = require('../utils/database-params'); @@ -86,6 +87,9 @@ function getQueryWithFilters (dataviewDefinition, params) { if (params.bbox) { var bboxFilter = new BBoxFilter({ column: 'the_geom_webmercator', srid: 3857 }, { bbox: params.bbox }); query = bboxFilter.sql(query); + } else if (params.circle) { + const circleFilter = new CircleFilter({ column: 'the_geom_webmercator', srid: 3857 }, { circle: params.circle }); + query = circleFilter.sql(query); } return query; @@ -197,6 +201,9 @@ function getQueryWithOwnFilters (dataviewDefinition, params) { if (params.bbox) { var bboxFilter = new BBoxFilter({ column: 'the_geom', srid: 4326 }, { bbox: params.bbox }); query = bboxFilter.sql(query); + } else if (params.circle) { + const circleFilter = new CircleFilter({ column: 'the_geom', srid: 4326 }, { circle: params.circle }); + query = circleFilter.sql(query); } return query; diff --git a/lib/models/filter/circle.js b/lib/models/filter/circle.js new file mode 100644 index 00000000..5dcd35db --- /dev/null +++ b/lib/models/filter/circle.js @@ -0,0 +1,58 @@ +'use strict'; + +const debug = require('debug')('windshaft:filter:circle'); +function filterQueryTpl ({ sql, column, srid, lng, lat, radius } = {}) { + return ` + SELECT + * + FROM (${sql}) _cdb_circle_filter + WHERE + ST_Intersects( + ${column}, + ST_Buffer( + ST_Transform( + ST_SetSRID(ST_Point(${lng},${lat}), 4326), + ${srid} + ), + ${radius} + ) + ) + `; +} + +module.exports = class CircleFilter { + constructor (filterDefinition, filterParams) { + const { circle } = filterParams; + + if (!circle) { + throw new Error('Circle filter expects to have a "circle" param'); + } + + const { lng, lat, radius } = JSON.parse(circle); + + if (!Number.isFinite(lng) || !Number.isFinite(lat) || !Number.isFinite(radius)) { + throw new Error('Missing parameter for Circle Filter, expected: "lng", "lat", and "radius"'); + } + + this.column = filterDefinition.column || 'the_geom_webmercator'; + this.srid = filterDefinition.srid || 3857; + this.lng = lng; + this.lat = lat; + this.radius = radius; + } + + sql (rawSql) { + const circleSql = filterQueryTpl({ + sql: rawSql, + column: this.column, + srid: this.srid, + lng: this.lng, + lat: this.lat, + radius: this.radius + }); + + debug(circleSql); + + return circleSql; + } +}; diff --git a/test/acceptance/dataviews/filters-test.js b/test/acceptance/dataviews/filters-test.js index 8e4743e5..dbb07a0a 100644 --- a/test/acceptance/dataviews/filters-test.js +++ b/test/acceptance/dataviews/filters-test.js @@ -77,8 +77,7 @@ describe('circle filter', function () { const scenarios = [ { - params: { - }, + params: JSON.stringify({}), expected: { type: 'aggregation', aggregation: 'sum', @@ -99,27 +98,24 @@ describe('circle filter', function () { }, { params: { - circle: { + circle: JSON.stringify({ lat: 0, lng: 0, radius: 5000 - } + }) }, expected: { type: 'aggregation', aggregation: 'sum', - count: 21, + count: 1, nulls: 0, nans: 0, infinities: 0, - min: 5, - max: 40, - categoriesCount: 4, + min: 1, + max: 1, + categoriesCount: 1, categories: [ - { category: 'category_4', value: 40, agg: false }, - { category: 'category_3', value: 9, agg: false }, - { category: 'category_2', value: 6, agg: false }, - { category: 'category_1', value: 5, agg: false } + { category: 'category_1', value: 1, agg: false } ] } } diff --git a/test/support/test-client.js b/test/support/test-client.js index 634509c0..b09fb552 100644 --- a/test/support/test-client.js +++ b/test/support/test-client.js @@ -485,7 +485,7 @@ TestClient.prototype.getDataview = function (dataviewName, params, callback) { urlParams.own_filter = params.own_filter; } - ['bbox', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function (extraParam) { + ['bbox', 'circle', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function (extraParam) { if (Object.prototype.hasOwnProperty.call(params, extraParam)) { urlParams[extraParam] = params[extraParam]; }