Merge pull request #971 from CartoDB/cartovl-130
Send dates as unix epoch instead strings in .mvt files
This commit is contained in:
1
NEWS.md
1
NEWS.md
@@ -33,6 +33,7 @@ New features:
|
||||
- nock: 9.2.6
|
||||
- strftime: 0.10.0
|
||||
- Optional instantiation metadata stats (https://github.com/CartoDB/Windshaft-cartodb/pull/952)
|
||||
- Experimental dates_as_numbers support
|
||||
|
||||
Bug Fixes:
|
||||
- Validates tile coordinates (z/x/y) from request params to be a valid integer value.
|
||||
|
||||
@@ -35,6 +35,7 @@ const TurboCartoAdapter = require('../models/mapconfig/adapter/turbo-carto-adapt
|
||||
const DataviewsWidgetsAdapter = require('../models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
const AggregationMapConfigAdapter = require('../models/mapconfig/adapter/aggregation-mapconfig-adapter');
|
||||
const MapConfigAdapter = require('../models/mapconfig/adapter');
|
||||
const VectorMapConfigAdapter = require('../models/mapconfig/adapter/vector-mapconfig-adapter');
|
||||
|
||||
const ResourceLocator = require('../models/resource-locator');
|
||||
const LayergroupMetadata = require('../utils/layergroup-metadata');
|
||||
@@ -135,6 +136,7 @@ module.exports = class ApiRouter {
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new VectorMapConfigAdapter(pgConnection),
|
||||
new AggregationMapConfigAdapter(pgConnection),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend),
|
||||
new TurboCartoAdapter()
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQu
|
||||
layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addDateWrappingMetadata (layergroup, mapConfig.obj());
|
||||
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig);
|
||||
|
||||
next();
|
||||
|
||||
@@ -34,12 +34,6 @@ MapnikLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
function queryPromise(dbConnection, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dbConnection.query(query, (err, res) => err ? reject(err) : resolve(res));
|
||||
});
|
||||
}
|
||||
|
||||
function columnAggregations(field) {
|
||||
if (field.type === 'number') {
|
||||
return ['min', 'max', 'avg', 'sum'];
|
||||
@@ -63,7 +57,7 @@ function _getSQL(ctx, query, type='pre', zoom=0) {
|
||||
}
|
||||
|
||||
function _estimatedFeatureCount(ctx) {
|
||||
return queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation))
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation))
|
||||
.then(res => ({ estimatedFeatureCount: res.rows[0].rows }))
|
||||
.catch(() => ({ estimatedFeatureCount: -1 }));
|
||||
}
|
||||
@@ -71,7 +65,7 @@ function _estimatedFeatureCount(ctx) {
|
||||
function _featureCount(ctx) {
|
||||
if (ctx.metaOptions.featureCount) {
|
||||
// TODO: if ctx.metaOptions.columnStats we can combine this with column stats query
|
||||
return queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount))
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount))
|
||||
.then(res => ({ featureCount: res.rows[0].rows }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -82,7 +76,7 @@ function _aggrFeatureCount(ctx) {
|
||||
// We expect as zoom level as the value of aggrFeatureCount
|
||||
// TODO: it'd be nice to admit an array of zoom levels to
|
||||
// return metadata for multiple levels.
|
||||
return queryPromise(
|
||||
return queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, queryUtils.getQueryActualRowCount, 'post', ctx.metaOptions.aggrFeatureCount)
|
||||
).then(res => ({ aggrFeatureCount: res.rows[0].rows }));
|
||||
@@ -93,7 +87,8 @@ function _aggrFeatureCount(ctx) {
|
||||
function _geometryType(ctx) {
|
||||
if (ctx.metaOptions.geometryType) {
|
||||
const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn();
|
||||
return queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn)))
|
||||
const sqlQuery = _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn));
|
||||
return queryUtils.queryPromise(ctx.dbConnection, sqlQuery)
|
||||
.then(res => ({ geometryType: res.rows[0].geom_type }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -102,7 +97,7 @@ function _geometryType(ctx) {
|
||||
function _columns(ctx) {
|
||||
if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) {
|
||||
// note: post-aggregation columns are in layer.options.columns when aggregation is present
|
||||
return queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)))
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)))
|
||||
.then(res => formatResultFields(ctx.dbConnection, res.fields));
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -156,7 +151,7 @@ function _sample(ctx, numRows) {
|
||||
const requestedRows = ctx.metaOptions.sample.num_rows || DEFAULT_SAMPLE_ROWS;
|
||||
const limit = Math.ceil(requestedRows * 1.5);
|
||||
let columns = ctx.metaOptions.sample.include_columns;
|
||||
return queryPromise(ctx.dbConnection, _getSQL(
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(
|
||||
ctx,
|
||||
sql => queryUtils.getQuerySample(sql, sampleProb, limit, SAMPLE_SEED, columns)
|
||||
)).then(res => ({ sample: res.rows }));
|
||||
@@ -186,7 +181,7 @@ function _columnStats(ctx, columns) {
|
||||
// TODO: ctx.metaOptions.columnStats.maxCategories
|
||||
// => use PG stats to dismiss columns with more distinct values
|
||||
queries.push(
|
||||
queryPromise(
|
||||
queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls))
|
||||
).then(res => ({ [name]: { categories: res.rows } }))
|
||||
@@ -194,7 +189,7 @@ function _columnStats(ctx, columns) {
|
||||
}
|
||||
});
|
||||
queries.push(
|
||||
queryPromise(
|
||||
queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`)
|
||||
).then(res => {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const queryUtils = require('../../../utils/query-utils');
|
||||
const dateWrapper = require('../../../utils/date-wrapper');
|
||||
|
||||
/**
|
||||
* This middleware wraps the layer query transforming the date fields into numbers because mvt tiles
|
||||
* doesnt support dates as primitive type.
|
||||
*
|
||||
* - This middleware is ONLY activated when the `dates_as_numbers` option is enabled for some layer in the mapConfig.
|
||||
*/
|
||||
class VectorMapConfigAdapter {
|
||||
constructor(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
getMapConfig(user, requestMapConfig, params, context, callback) {
|
||||
if (!this._isDatesAsNumbersFlagEnabled(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this._wrapDates(requestMapConfig, user)
|
||||
.then(updatedRequestMapConfig => callback(null, updatedRequestMapConfig))
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
_wrapDates(requestMapConfig, user) {
|
||||
return Promise.all(requestMapConfig.layers.map(layer => this._wrapLayer(layer, user)))
|
||||
.then(() => requestMapConfig);
|
||||
}
|
||||
|
||||
_wrapLayer(layer, user) {
|
||||
if (!layer.options.dates_as_numbers || !layer.options.sql) {
|
||||
return Promise.resolve(layer);
|
||||
}
|
||||
const originalQuery = layer.options.sql;
|
||||
return this._getColumns(user, originalQuery)
|
||||
.then(result => {
|
||||
const newSqlQuery = dateWrapper.wrapDates(originalQuery, result.fields);
|
||||
layer.options.sql = newSqlQuery;
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
_getColumns(user, originalQuery) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const query = queryUtils.getQueryLimited(originalQuery, 0);
|
||||
queryUtils.queryPromise(connection, query)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isDatesAsNumbersFlagEnabled(requestMapConfig) {
|
||||
return requestMapConfig.layers && requestMapConfig.layers.some(layer => layer.options.dates_as_numbers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = VectorMapConfigAdapter;
|
||||
59
lib/cartodb/utils/date-wrapper.js
Normal file
59
lib/cartodb/utils/date-wrapper.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const DATE_OIDS = Object.freeze({
|
||||
1082: 'DATE',
|
||||
1083: 'TIME',
|
||||
1114: 'TIMESTAMP',
|
||||
1184: 'TIMESTAMPTZ',
|
||||
1266: 'TIMETZ'
|
||||
});
|
||||
|
||||
/**
|
||||
* Wrap a query transforming all date columns into a unix epoch
|
||||
* @param {*} originalQuery
|
||||
* @param {*} fields
|
||||
*/
|
||||
function wrapDates(originalQuery, fields) {
|
||||
return `
|
||||
SELECT
|
||||
${fields.map(field => _isDateType(field) ? _castColumnToEpoch(field.name) : `${field.name}`).join(',')}
|
||||
FROM
|
||||
(${originalQuery}) _cdb_epoch_transformation `;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} field
|
||||
*/
|
||||
function _isDateType(field) {
|
||||
return DATE_OIDS.hasOwnProperty(field.dataTypeID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sql query to transform a date column into a unix epoch
|
||||
* @param {string} column - The name of the date column
|
||||
*/
|
||||
function _castColumnToEpoch(columnName) {
|
||||
return `date_part('epoch', "${columnName}") as "${columnName}"`;
|
||||
}
|
||||
|
||||
function getColumnsWithWrappedDates(query) {
|
||||
if(!query){
|
||||
return;
|
||||
}
|
||||
if (!query.match(/\b_cdb_epoch_transformation\b/)) {
|
||||
return;
|
||||
}
|
||||
const columns = [];
|
||||
const fieldMatcher = /\bdate_part\('epoch', "([^"]+)"\) as "([^"]+)"/gmi;
|
||||
let match;
|
||||
do {
|
||||
match = fieldMatcher.exec(query);
|
||||
if (match && match[1] === match[2]) {
|
||||
columns.push(match[1]);
|
||||
}
|
||||
} while (match);
|
||||
return columns;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wrapDates,
|
||||
getColumnsWithWrappedDates
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
const dateWrapper = require('./date-wrapper');
|
||||
|
||||
module.exports = class LayergroupMetadata {
|
||||
constructor (resourceLocator) {
|
||||
this.resourceLocator = resourceLocator;
|
||||
@@ -165,4 +167,21 @@ module.exports = class LayergroupMetadata {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addDateWrappingMetadata (layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
const mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
const layerOptions = mapConfigLayer.options;
|
||||
if (layerOptions.dates_as_numbers && layerOptions.sql) {
|
||||
const wrappedColumns = dateWrapper.getColumnsWithWrappedDates(layerOptions.sql);
|
||||
if (wrappedColumns) {
|
||||
layer.meta.dates_as_numbers = wrappedColumns;
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
function prepareQuery(sql) {
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1');
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1');
|
||||
}
|
||||
|
||||
module.exports.extractTableNames = function extractTableNames(query) {
|
||||
@@ -59,25 +59,25 @@ module.exports.handleFloatColumn = function handleFloatColumn(ctx) {
|
||||
};
|
||||
|
||||
/** Count NULL appearances */
|
||||
module.exports.countNULLs= function countNULLs(ctx) {
|
||||
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` :
|
||||
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` :
|
||||
return `${!ctx.isFloatColumn ? '0' :
|
||||
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
|
||||
module.exports.getQueryTopCategories = function(query, column, topN, includeNulls=false) {
|
||||
module.exports.getQueryTopCategories = function (query, column, topN, includeNulls = false) {
|
||||
const where = includeNulls ? '' : `WHERE ${column} IS NOT NULL`;
|
||||
return `
|
||||
SELECT ${column} AS category, COUNT(*) AS frequency
|
||||
@@ -101,7 +101,7 @@ function columnSelector(columns) {
|
||||
throw new TypeError(`Bad argument type for columns: ${typeof columns}`);
|
||||
}
|
||||
|
||||
module.exports.getQuerySample = function(query, sampleProb, limit = null, randomSeed = 0.5, columns = null) {
|
||||
module.exports.getQuerySample = function (query, sampleProb, limit = null, randomSeed = 0.5, columns = null) {
|
||||
const singleTable = simpleQueryTable(query);
|
||||
if (singleTable) {
|
||||
return getTableSample(singleTable.table, columns || singleTable.columns, sampleProb, limit, randomSeed);
|
||||
@@ -121,7 +121,7 @@ module.exports.getQuerySample = function(query, sampleProb, limit = null, random
|
||||
function getTableSample(table, columns, sampleProb, limit = null, randomSeed = 0.5) {
|
||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||
sampleProb *= 100;
|
||||
randomSeed *= Math.pow(2, 31) -1;
|
||||
randomSeed *= Math.pow(2, 31) - 1;
|
||||
return `
|
||||
SELECT ${columnSelector(columns)}
|
||||
FROM ${table}
|
||||
@@ -132,12 +132,12 @@ function getTableSample(table, columns, sampleProb, limit = null, randomSeed = 0
|
||||
function simpleQueryTable(sql) {
|
||||
const basicQuery =
|
||||
/\s*SELECT\s+([\*a-z0-9_,\s]+?)\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i;
|
||||
const unwrappedQuery = new RegExp("^"+basicQuery.source+"$", 'i');
|
||||
const unwrappedQuery = new RegExp('^' + basicQuery.source + '$', 'i');
|
||||
// queries for named maps are wrapped like this:
|
||||
var wrappedQuery = new RegExp(
|
||||
"^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(" +
|
||||
'^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(' +
|
||||
basicQuery.source +
|
||||
"\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$",
|
||||
'\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$',
|
||||
'i'
|
||||
);
|
||||
let match = sql.match(unwrappedQuery);
|
||||
@@ -147,13 +147,13 @@ function simpleQueryTable(sql) {
|
||||
if (match) {
|
||||
const columns = match[1];
|
||||
const schema = match[3];
|
||||
const table = match[4];
|
||||
const table = match[4];
|
||||
return { table: schema ? `${schema}.${table}` : table, columns };
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports.getQueryGeometryType = function(query, geometryColumn) {
|
||||
module.exports.getQueryGeometryType = function (query, geometryColumn) {
|
||||
return `
|
||||
SELECT ST_GeometryType(${geometryColumn}) AS geom_type
|
||||
FROM (${query}) AS __cdb_query
|
||||
@@ -162,10 +162,19 @@ module.exports.getQueryGeometryType = function(query, geometryColumn) {
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports.getQueryLimited = function(query, limit=0) {
|
||||
function getQueryLimited(query, limit = 0) {
|
||||
return `
|
||||
SELECT *
|
||||
FROM (${query}) AS __cdb_query
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
function queryPromise(dbConnection, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dbConnection.query(query, (err, res) => err ? reject(err) : resolve(res));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.queryPromise = queryPromise;
|
||||
module.exports.getQueryLimited = getQueryLimited;
|
||||
265
test/acceptance/date-wrapping.spec.js
Normal file
265
test/acceptance/date-wrapping.spec.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/* eslint-env mocha */
|
||||
const assert = require('assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
const mapConfigFactory = require('../fixtures/test_mapconfigFactory');
|
||||
|
||||
describe('date-wrapping', () => {
|
||||
let testClient;
|
||||
|
||||
describe('when a map instantiation has one single layer', () => {
|
||||
describe('and the layer has the "dates_as_numbers" option enabled', () => {
|
||||
beforeEach(() => {
|
||||
const mapConfig = mapConfigFactory.getVectorMapConfig({ layerOptions: [{ dates_as_numbers: true }]});
|
||||
testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
afterEach(done => testClient.drain(done));
|
||||
|
||||
it('should return date columns casted as numbers', done => {
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => {
|
||||
const expected = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 }
|
||||
}
|
||||
];
|
||||
const actual = JSON.parse(mvt.toGeoJSONSync(0)).features;
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return metadata with casted columns', done => {
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers, ['date']);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and the layer has the "dates_as_numbers" option disabled', () => {
|
||||
beforeEach(() => {
|
||||
const mapConfig = mapConfigFactory.getVectorMapConfig({ layerOptions: [{ dates_as_numbers: false }]});
|
||||
testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
afterEach(done => testClient.drain(done));
|
||||
|
||||
it('should return date columns as dates', done => {
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => {
|
||||
const expected = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1 }
|
||||
}
|
||||
];
|
||||
const actual = JSON.parse(mvt.toGeoJSONSync(0)).features;
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a map instantiation has multiple layers', () => {
|
||||
afterEach(done => testClient.drain(done));
|
||||
|
||||
describe('and both layers have the "dates_as_numbers" option enabled', () => {
|
||||
beforeEach(() => {
|
||||
const mapConfig = mapConfigFactory.getVectorMapConfig({
|
||||
numberOfLayers: 2,
|
||||
layerOptions: [
|
||||
{ dates_as_numbers: true },
|
||||
{ dates_as_numbers: true }
|
||||
]
|
||||
});
|
||||
testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
it('should return dates as numbers for every layer', done => {
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers, ['date']);
|
||||
assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers, ['date']);
|
||||
});
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => {
|
||||
const expected0 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 }
|
||||
}
|
||||
];
|
||||
const expected1 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 }
|
||||
}
|
||||
];
|
||||
const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features;
|
||||
const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features;
|
||||
|
||||
assert.deepEqual(actual0, expected0);
|
||||
assert.deepEqual(actual1, expected1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and only one layers has the "dates_as_numbers" option enabled', () => {
|
||||
beforeEach(() => {
|
||||
const mapConfig = mapConfigFactory.getVectorMapConfig({
|
||||
numberOfLayers: 2,
|
||||
layerOptions: [
|
||||
{ dates_as_numbers: false },
|
||||
{ dates_as_numbers: true }
|
||||
]
|
||||
});
|
||||
testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
it('should return dates as numbers only for the layer with the "dates_as_numbers" flag enabled', done => {
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers || [], []);
|
||||
assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers, ['date']);
|
||||
});
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => {
|
||||
const expected0 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1 }
|
||||
}
|
||||
];
|
||||
const expected1 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 }
|
||||
}
|
||||
];
|
||||
const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features;
|
||||
const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features;
|
||||
|
||||
assert.deepEqual(actual0, expected0);
|
||||
assert.deepEqual(actual1, expected1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and none of the layers has the "dates_as_numbers" option enabled', () => {
|
||||
beforeEach(() => {
|
||||
const mapConfig = mapConfigFactory.getVectorMapConfig({
|
||||
numberOfLayers: 2,
|
||||
layerOptions: [
|
||||
{ dates_as_numbers: false },
|
||||
{ dates_as_numbers: false }
|
||||
]
|
||||
});
|
||||
testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
it('should return dates as dates for both layers', done => {
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers || [], []);
|
||||
assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers || [], []);
|
||||
});
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => {
|
||||
const expected0 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1 }
|
||||
}
|
||||
];
|
||||
const expected1 = [
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 1,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 0 }
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
id: 2,
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { _cdb_feature_count: 1, cartodb_id: 1 }
|
||||
}
|
||||
];
|
||||
const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features;
|
||||
const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features;
|
||||
|
||||
assert.deepEqual(actual0, expected0);
|
||||
assert.deepEqual(actual1, expected1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
62
test/fixtures/test_mapconfigFactory.js
vendored
Normal file
62
test/fixtures/test_mapconfigFactory.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
function getVectorMapConfig(opts) {
|
||||
return {
|
||||
buffersize: {
|
||||
mvt: 1
|
||||
},
|
||||
layers: _generateLayers(opts),
|
||||
};
|
||||
}
|
||||
|
||||
function _generateLayers(opts) {
|
||||
const numberOfLayers = opts.numberOfLayers || 1;
|
||||
const layers = [];
|
||||
for (let index = 0; index < numberOfLayers; index++) {
|
||||
const layerOptions = (opts.layerOptions || {})[index] || {};
|
||||
layers.push(_generateLayerConfig(layerOptions));
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
|
||||
function _generateLayerConfig(opts) {
|
||||
return {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: `
|
||||
SELECT
|
||||
(DATE '2018-06-01' + x) as date,
|
||||
x as cartodb_id,
|
||||
st_makepoint(x * 10, x * 10) as the_geom,
|
||||
st_makepoint(x * 10, x * 10) as the_geom_webmercator
|
||||
FROM
|
||||
generate_series(0, 1) x`,
|
||||
aggregation: {
|
||||
columns: {},
|
||||
dimensions: {
|
||||
date: 'date'
|
||||
},
|
||||
placement: 'centroid',
|
||||
resolution: 1,
|
||||
threshold: 1
|
||||
},
|
||||
dates_as_numbers: opts.dates_as_numbers,
|
||||
metadata: {
|
||||
geometryType: true,
|
||||
columnStats: {
|
||||
topCategories: 32768,
|
||||
includeNulls: true
|
||||
},
|
||||
sample: {
|
||||
num_rows: 1000,
|
||||
include_columns: [
|
||||
'date'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = { getVectorMapConfig };
|
||||
Reference in New Issue
Block a user