Initial commit
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
var _ = require('underscore');
|
||||
var VisView = require('../vis/vis-view');
|
||||
var VisModel = require('../vis/vis');
|
||||
var Loader = require('../core/loader');
|
||||
var VizJSON = require('./vizjson');
|
||||
|
||||
var DEFAULT_OPTIONS = {
|
||||
tiles_loader: true,
|
||||
loaderControl: true,
|
||||
infowindow: true, // TODO: it seems that this is no longer used
|
||||
tooltip: true, // TODO: it seems that this is no longer used
|
||||
logo: true,
|
||||
show_empty_infowindow_fields: false,
|
||||
showLimitErrors: false,
|
||||
interactiveFeatures: false
|
||||
};
|
||||
|
||||
var createVis = function (el, vizjson, options) {
|
||||
if (typeof el === 'string') {
|
||||
el = document.getElementById(el);
|
||||
}
|
||||
if (!el) {
|
||||
throw new TypeError('a valid DOM element or selector must be provided');
|
||||
}
|
||||
if (!vizjson) {
|
||||
throw new TypeError('a vizjson URL or object must be provided');
|
||||
}
|
||||
|
||||
var isProtocolHTTPs = window && window.location.protocol && window.location.protocol === 'https:';
|
||||
options = _.defaults(options || {}, DEFAULT_OPTIONS);
|
||||
|
||||
var visModel = new VisModel({
|
||||
apiKey: options.apiKey,
|
||||
authToken: options.authToken,
|
||||
showEmptyInfowindowFields: options.show_empty_infowindow_fields === true,
|
||||
showLimitErrors: options.showLimitErrors === true,
|
||||
https: isProtocolHTTPs || options.https === true,
|
||||
interactiveFeatures: options.interactiveFeatures
|
||||
});
|
||||
|
||||
new VisView({ // eslint-disable-line
|
||||
el: el,
|
||||
model: visModel,
|
||||
settingsModel: visModel.settings
|
||||
});
|
||||
|
||||
if (typeof vizjson === 'string') {
|
||||
var url = vizjson;
|
||||
Loader.get(url, function (vizjson) {
|
||||
if (vizjson) {
|
||||
loadVizJSON(el, visModel, vizjson, options);
|
||||
} else {
|
||||
throw new Error('error fetching viz.json file');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
loadVizJSON(el, visModel, vizjson, options);
|
||||
}
|
||||
|
||||
return visModel;
|
||||
};
|
||||
|
||||
var loadVizJSON = function (el, visModel, vizjsonData, options) {
|
||||
var vizjson = new VizJSON(vizjsonData);
|
||||
applyOptionsToVizJSON(vizjson, options);
|
||||
|
||||
var showLegends = true;
|
||||
if (_.isBoolean(options.legends)) {
|
||||
showLegends = options.legends;
|
||||
} else if (vizjson.options && _.isBoolean(vizjson.options.legends)) {
|
||||
showLegends = vizjson.options.legends;
|
||||
}
|
||||
|
||||
var showLayerSelector = true;
|
||||
if (_.isBoolean(options.layer_selector)) {
|
||||
showLayerSelector = options.layer_selector;
|
||||
} else if (vizjson.options && _.isBoolean(vizjson.options.layer_selector)) {
|
||||
showLayerSelector = vizjson.options.layer_selector;
|
||||
}
|
||||
|
||||
var layerSelectorEnabled = true;
|
||||
if (_.isBoolean(options.layerSelectorEnabled)) {
|
||||
layerSelectorEnabled = options.layerSelectorEnabled;
|
||||
}
|
||||
|
||||
visModel.set({
|
||||
title: vizjson.title,
|
||||
description: vizjson.description,
|
||||
https: visModel.get('https') || vizjson.https === true
|
||||
});
|
||||
|
||||
visModel.setSettings({
|
||||
showLegends: showLegends,
|
||||
showLayerSelector: showLayerSelector,
|
||||
layerSelectorEnabled: layerSelectorEnabled
|
||||
});
|
||||
|
||||
visModel.load(vizjson);
|
||||
|
||||
if (!options.skipMapInstantiation) {
|
||||
visModel.instantiateMap();
|
||||
}
|
||||
};
|
||||
|
||||
var applyOptionsToVizJSON = function (vizjson, options) {
|
||||
vizjson.options = vizjson.options || {};
|
||||
vizjson.options.scrollwheel = _.isBoolean(options.scrollwheel) ? options.scrollwheel : vizjson.options.scrollwheel;
|
||||
|
||||
if (!options.tiles_loader || !options.loaderControl) {
|
||||
vizjson.removeLoaderOverlay();
|
||||
}
|
||||
|
||||
if (options.searchControl === true) {
|
||||
vizjson.addSearchOverlay();
|
||||
} else if (options.searchControl === false) {
|
||||
vizjson.removeSearchOverlay();
|
||||
}
|
||||
|
||||
if ((options.title && vizjson.title) || (options.description && vizjson.description)) {
|
||||
vizjson.addHeaderOverlay(options.title, options.description, options.shareable);
|
||||
}
|
||||
|
||||
if (options.zoomControl !== undefined && !options.zoomControl) {
|
||||
vizjson.removeZoomOverlay();
|
||||
}
|
||||
|
||||
if (options.logo === false) {
|
||||
vizjson.removeLogoOverlay();
|
||||
}
|
||||
|
||||
if (_.has(options, 'vector')) {
|
||||
vizjson.setVector(options.vector);
|
||||
}
|
||||
|
||||
// if bounds are present zoom and center will not taken into account
|
||||
var zoom = parseInt(options.zoom, 10);
|
||||
if (!isNaN(zoom)) {
|
||||
vizjson.setZoom(zoom);
|
||||
}
|
||||
|
||||
// Center coordinates?
|
||||
var centerLat = parseFloat(options.center_lat);
|
||||
var centerLon = parseFloat(options.center_lon);
|
||||
if (!isNaN(centerLat) && !isNaN(centerLon)) {
|
||||
vizjson.setCenter([centerLat, centerLon]);
|
||||
}
|
||||
|
||||
// Center object
|
||||
if (options.center !== undefined) {
|
||||
vizjson.setCenter(options.center);
|
||||
}
|
||||
|
||||
// Bounds?
|
||||
var swLat = parseFloat(options.sw_lat);
|
||||
var swLon = parseFloat(options.sw_lon);
|
||||
var neLat = parseFloat(options.ne_lat);
|
||||
var neLon = parseFloat(options.ne_lon);
|
||||
|
||||
if (!isNaN(swLat) && !isNaN(swLon) && !isNaN(neLat) && !isNaN(neLon)) {
|
||||
vizjson.setBounds([
|
||||
[ swLat, swLon ],
|
||||
[ neLat, neLon ]
|
||||
]);
|
||||
}
|
||||
|
||||
if (options.gmaps_base_type) {
|
||||
vizjson.enforceGMapsBaseLayer(options.gmaps_base_type, options.gmaps_style);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = createVis;
|
||||
@@ -0,0 +1,17 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
// NOTE only for usage in non-core bundles (where Backbone is available)
|
||||
function Promise () {
|
||||
}
|
||||
|
||||
_.extend(Promise.prototype, Backbone.Events, {
|
||||
done: function (fn) {
|
||||
return this.bind('done', fn);
|
||||
},
|
||||
error: function (fn) {
|
||||
return this.bind('error', fn);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Promise;
|
||||
@@ -0,0 +1,704 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Mustache = require('mustache');
|
||||
var Promise = require('./promise');
|
||||
|
||||
var NO_BOUNDS_ERROR_MESSAGE = 'No bounds';
|
||||
|
||||
// Variable that defines if a query should be using get method or post method
|
||||
var MAX_LENGTH_GET_QUERY = 1024;
|
||||
|
||||
function SQL (options) {
|
||||
if (window.cdb === this || window === this) {
|
||||
return new SQL(options);
|
||||
}
|
||||
if (!options.user) {
|
||||
throw new Error('user should be provided');
|
||||
}
|
||||
var loc = String(window.location.protocol);
|
||||
loc = loc.slice(0, loc.length - 1);
|
||||
if (loc === 'file') {
|
||||
loc = 'https';
|
||||
}
|
||||
|
||||
this.options = _.defaults(options, {
|
||||
version: 'v2',
|
||||
protocol: loc,
|
||||
jsonp: !$.support.cors,
|
||||
abortable: false
|
||||
});
|
||||
|
||||
if (!this.options.sql_api_template) {
|
||||
var opts = this.options;
|
||||
var template = null;
|
||||
if (opts && opts.completeDomain) {
|
||||
template = opts.completeDomain;
|
||||
} else {
|
||||
var host = opts.host || 'carto.com';
|
||||
var protocol = opts.protocol || 'https';
|
||||
template = protocol + '://{user}.' + host;
|
||||
}
|
||||
this.options.sql_api_template = template;
|
||||
}
|
||||
}
|
||||
|
||||
SQL.prototype._host = function () {
|
||||
var opts = this.options;
|
||||
return opts.sql_api_template.replace('{user}', opts.user) + '/api/' + opts.version + '/sql';
|
||||
};
|
||||
|
||||
/**
|
||||
* var sql = new SQL('cartodb_username');
|
||||
* sql.execute("select * from {{ table }} where id = {{ id }}", {
|
||||
* table: 'test',
|
||||
* id: '1'
|
||||
* })
|
||||
*/
|
||||
SQL.prototype.execute = function (sql, vars, options, callback) {
|
||||
var promise = new Promise();
|
||||
if (!sql) {
|
||||
throw new TypeError('sql should not be null');
|
||||
}
|
||||
// setup arguments
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
if (_.isFunction(fn)) {
|
||||
callback = fn;
|
||||
}
|
||||
options = _.defaults(options || {}, this.options);
|
||||
var params = {
|
||||
type: 'get',
|
||||
dataType: 'json',
|
||||
crossDomain: true
|
||||
};
|
||||
|
||||
if (this._xhr && this.options.abortable === true) {
|
||||
this._xhr.abort();
|
||||
}
|
||||
|
||||
if (options.cache !== undefined) {
|
||||
params.cache = options.cache;
|
||||
}
|
||||
|
||||
if (options.jsonp) {
|
||||
delete params.crossDomain;
|
||||
if (options.jsonpCallback) {
|
||||
params.jsonpCallback = options.jsonpCallback;
|
||||
}
|
||||
params.dataType = 'jsonp';
|
||||
}
|
||||
|
||||
// Substitute mapnik tokens
|
||||
// resolution at zoom level 0
|
||||
var res = '156543.03515625';
|
||||
// full webmercator extent
|
||||
var ext = 'ST_MakeEnvelope(-20037508.5,-20037508.5,20037508.5,20037508.5,3857)';
|
||||
sql = sql.replace('!bbox!', ext)
|
||||
.replace('!pixel_width!', res)
|
||||
.replace('!pixel_height!', res);
|
||||
|
||||
// create query
|
||||
var query = Mustache.render(sql, vars);
|
||||
|
||||
// check method: if we are going to send by get or by post
|
||||
var isGetRequest = query.length < MAX_LENGTH_GET_QUERY;
|
||||
|
||||
// generate url depending on the http method
|
||||
var reqParams = ['format', 'dp', 'api_key'];
|
||||
// request params
|
||||
if (options.extra_params) {
|
||||
reqParams = reqParams.concat(options.extra_params);
|
||||
}
|
||||
|
||||
params.url = this._host();
|
||||
var i, r, v;
|
||||
if (isGetRequest) {
|
||||
var q = 'q=' + encodeURIComponent(query);
|
||||
for (i in reqParams) {
|
||||
r = reqParams[i];
|
||||
v = options[r];
|
||||
if (v != null) {
|
||||
q += '&' + r + '=' + v;
|
||||
}
|
||||
}
|
||||
|
||||
params.url += '?' + q;
|
||||
} else {
|
||||
var objPost = {'q': query};
|
||||
for (i in reqParams) {
|
||||
r = reqParams[i];
|
||||
v = options[r];
|
||||
if (v != null) {
|
||||
objPost[r] = v;
|
||||
}
|
||||
}
|
||||
|
||||
params.data = objPost;
|
||||
params.type = 'post';
|
||||
}
|
||||
|
||||
// wrap success and error functions
|
||||
var success = options.success;
|
||||
var error = options.error;
|
||||
if (success) delete options.success;
|
||||
if (error) delete error.success;
|
||||
|
||||
params.error = function (resp) {
|
||||
var res = resp.responseText || resp.response;
|
||||
var errors = res && JSON.parse(res);
|
||||
promise.trigger('error', errors && errors.error, resp);
|
||||
if (error) error(resp);
|
||||
if (callback) callback(resp);
|
||||
};
|
||||
params.success = function (resp, status, xhr) {
|
||||
// manage rewest
|
||||
if (status === undefined) {
|
||||
status = resp.status;
|
||||
xhr = resp;
|
||||
resp = JSON.parse(resp.response);
|
||||
}
|
||||
// Timeout explanation. CartoDB.js ticket #336
|
||||
// From St.Ov.: "what setTimeout does is add a new event to the browser event queue
|
||||
// and the rendering engine is already in that queue (not entirely true, but close enough)
|
||||
// so it gets executed before the setTimeout event."
|
||||
setTimeout(function () {
|
||||
promise.trigger('done', resp, status, xhr);
|
||||
if (success) success(resp, status, xhr);
|
||||
if (callback) callback(null, resp);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
params.complete = function () {
|
||||
this._xhr = null;
|
||||
}.bind(this);
|
||||
|
||||
// call ajax
|
||||
delete options.jsonp;
|
||||
this._xhr = $.ajax(_.extend(params, options));
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
SQL.prototype.getBounds = function (sql, vars, options, callback) {
|
||||
var promise = new Promise();
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
if (_.isFunction(fn)) {
|
||||
callback = fn;
|
||||
}
|
||||
var s = 'SELECT ST_XMin(ST_Extent(the_geom)) as minx,' +
|
||||
' ST_YMin(ST_Extent(the_geom)) as miny,' +
|
||||
' ST_XMax(ST_Extent(the_geom)) as maxx,' +
|
||||
' ST_YMax(ST_Extent(the_geom)) as maxy' +
|
||||
' from ({{{ sql }}}) as subq';
|
||||
sql = Mustache.render(sql, vars);
|
||||
this.execute(s, { sql: sql }, options)
|
||||
.done(function (result) {
|
||||
if (result.rows && result.rows.length > 0 && result.rows[0].maxx != null) {
|
||||
var c = result.rows[0];
|
||||
var minlat = -85.0511;
|
||||
var maxlat = 85.0511;
|
||||
var minlon = -179;
|
||||
var maxlon = 179;
|
||||
|
||||
var clamp = function (x, min, max) {
|
||||
return x < min ? min : x > max ? max : x;
|
||||
};
|
||||
|
||||
var lon0 = clamp(c.maxx, minlon, maxlon);
|
||||
var lon1 = clamp(c.minx, minlon, maxlon);
|
||||
var lat0 = clamp(c.maxy, minlat, maxlat);
|
||||
var lat1 = clamp(c.miny, minlat, maxlat);
|
||||
|
||||
var bounds = [[lat0, lon0], [lat1, lon1]];
|
||||
promise.trigger('done', bounds);
|
||||
callback && callback(null, bounds);
|
||||
} else {
|
||||
var err = [NO_BOUNDS_ERROR_MESSAGE];
|
||||
promise.trigger('error', err);
|
||||
callback && callback(err);
|
||||
}
|
||||
})
|
||||
.error(function (err) {
|
||||
promise.trigger('error', err);
|
||||
callback && callback(err);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* var people_under_10 = sql
|
||||
* .table('test')
|
||||
* .columns(['age', 'column2'])
|
||||
* .filter('age < 10')
|
||||
* .limit(15)
|
||||
* .order_by('age')
|
||||
*
|
||||
* people_under_10(function(results) {
|
||||
* })
|
||||
*/
|
||||
|
||||
SQL.prototype.table = function (name) {
|
||||
var _name = name;
|
||||
var _filters;
|
||||
var _columns = [];
|
||||
var _limit;
|
||||
var _order;
|
||||
var _orderDir;
|
||||
var _sql = this;
|
||||
|
||||
function _table () {
|
||||
_table.fetch.apply(_table, arguments);
|
||||
}
|
||||
|
||||
_table.fetch = function (vars) {
|
||||
vars = vars || {};
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
if (_.isFunction(fn) && args.length === 1) {
|
||||
vars = {};
|
||||
}
|
||||
_sql.execute(_table.sql(), vars, fn);
|
||||
};
|
||||
|
||||
_table.sql = function () {
|
||||
var s = 'select';
|
||||
if (_columns.length) {
|
||||
s += ' ' + _columns.join(',') + ' ';
|
||||
} else {
|
||||
s += ' * ';
|
||||
}
|
||||
|
||||
s += 'from ' + _name;
|
||||
|
||||
if (_filters) {
|
||||
s += ' where ' + _filters;
|
||||
}
|
||||
if (_limit) {
|
||||
s += ' limit ' + _limit;
|
||||
}
|
||||
if (_order) {
|
||||
s += ' order by ' + _order;
|
||||
}
|
||||
if (_orderDir) {
|
||||
s += ' ' + _orderDir;
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
_table.filter = function (f) {
|
||||
_filters = f;
|
||||
return _table;
|
||||
};
|
||||
|
||||
_table.order_by = function (o) {
|
||||
_order = o;
|
||||
return _table;
|
||||
};
|
||||
_table.asc = function () {
|
||||
_orderDir = 'asc';
|
||||
return _table;
|
||||
};
|
||||
|
||||
_table.desc = function () {
|
||||
_orderDir = 'desc';
|
||||
return _table;
|
||||
};
|
||||
|
||||
_table.columns = function (c) {
|
||||
_columns = c;
|
||||
return _table;
|
||||
};
|
||||
|
||||
_table.limit = function (l) {
|
||||
_limit = l;
|
||||
return _table;
|
||||
};
|
||||
|
||||
return _table;
|
||||
};
|
||||
|
||||
/*
|
||||
* sql.filter(sql.f().distance('< 10km')
|
||||
*/
|
||||
/* SQL.geoFilter = function() {
|
||||
var _sql;
|
||||
function f() {}
|
||||
|
||||
f.distance = function(qty) {
|
||||
qty.replace('km', '*1000')
|
||||
_sql += 'st_distance(the_geom) ' + qty
|
||||
}
|
||||
f.or = function() {
|
||||
}
|
||||
|
||||
f.and = function() {
|
||||
}
|
||||
return f;
|
||||
}
|
||||
*/
|
||||
function arrayAgg (s) {
|
||||
return JSON.parse(s.replace(/^{/, '[').replace(/}$/, ']'));
|
||||
}
|
||||
|
||||
SQL.prototype.describeString = function (sql, column, callback) {
|
||||
var s = [
|
||||
'WITH t as (',
|
||||
' SELECT count(*) as total,',
|
||||
' count(DISTINCT {{column}}) as ndist',
|
||||
' FROM ({{sql}}) _wrap',
|
||||
' ), a as (',
|
||||
' SELECT ',
|
||||
' count(*) cnt, ',
|
||||
' {{column}}',
|
||||
' FROM ',
|
||||
' ({{sql}}) _wrap ',
|
||||
' GROUP BY ',
|
||||
' {{column}} ',
|
||||
' ORDER BY ',
|
||||
' cnt DESC',
|
||||
' ), b As (',
|
||||
' SELECT',
|
||||
' row_number() OVER (ORDER BY cnt DESC) rn,',
|
||||
' cnt',
|
||||
' FROM a',
|
||||
' ), c As (',
|
||||
' SELECT ',
|
||||
' sum(cnt) OVER (ORDER BY rn ASC) / t.total cumperc,',
|
||||
' rn,',
|
||||
' cnt ',
|
||||
' FROM b, t',
|
||||
' LIMIT 10',
|
||||
' ),',
|
||||
'stats as (',
|
||||
'select count(distinct({{column}})) as uniq, ',
|
||||
' count(*) as cnt, ',
|
||||
' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric as null_count, ',
|
||||
' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio, ',
|
||||
// ' CDB_DistinctMeasure(array_agg({{column}}::text)) as cat_weight ',
|
||||
' (SELECT max(cumperc) weight FROM c) As skew ',
|
||||
'from ({{sql}}) __wrap',
|
||||
'),',
|
||||
'hist as (',
|
||||
'select array_agg(row(d, c)) array_agg from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a',
|
||||
')',
|
||||
'select * from stats, hist'
|
||||
];
|
||||
|
||||
var query = Mustache.render(s.join('\n'), {
|
||||
column: column,
|
||||
sql: sql
|
||||
});
|
||||
|
||||
var normalizeName = function (str) {
|
||||
var normalizedStr = str.replace(/^"(.+(?="$))?"$/, '$1'); // removes surrounding quotes
|
||||
return normalizedStr.replace(/""/g, '"'); // removes duplicated quotes
|
||||
};
|
||||
|
||||
this.execute(query, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = data.rows[0];
|
||||
var weight = 0;
|
||||
var histogram = [];
|
||||
|
||||
try {
|
||||
var s = arrayAgg(row.array_agg);
|
||||
|
||||
histogram = _(s).map(function (row) {
|
||||
var r = row.match(/\((.*),(\d+)/);
|
||||
var name = normalizeName(r[1]);
|
||||
return [name, +r[2]];
|
||||
});
|
||||
|
||||
weight = row.skew * (1 - row.null_ratio) * (1 - row.uniq / row.cnt) * (row.uniq > 1 ? 1 : 0);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
type: 'string',
|
||||
hist: histogram,
|
||||
distinct: row.uniq,
|
||||
count: row.cnt,
|
||||
null_count: row.null_count,
|
||||
null_ratio: row.null_ratio,
|
||||
skew: row.skew,
|
||||
weight: weight
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SQL.prototype.describeDate = function (sql, column, callback) {
|
||||
var s = [
|
||||
'with minimum as (',
|
||||
'SELECT min({{column}}) as start_time FROM ({{sql}}) _wrap), ',
|
||||
'maximum as (SELECT max({{column}}) as end_time FROM ({{sql}}) _wrap), ',
|
||||
'null_ratio as (SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ',
|
||||
'moments as (SELECT count(DISTINCT {{column}}) as moments FROM ({{sql}}) _wrap)',
|
||||
'SELECT * FROM minimum, maximum, moments, null_ratio'
|
||||
];
|
||||
var query = Mustache.render(s.join('\n'), {
|
||||
column: column,
|
||||
sql: sql
|
||||
});
|
||||
|
||||
this.execute(query, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = data.rows[0];
|
||||
var e = new Date(row.end_time);
|
||||
var s = new Date(row.start_time);
|
||||
|
||||
var steps = Math.min(row.moments, 1024);
|
||||
|
||||
callback(null, {
|
||||
type: 'date',
|
||||
start_time: s,
|
||||
end_time: e,
|
||||
range: e - s,
|
||||
steps: steps,
|
||||
null_ratio: row.null_ratio
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SQL.prototype.describeBoolean = function (sql, column, callback) {
|
||||
var s = [
|
||||
'with stats as (',
|
||||
'select count(distinct({{column}})) as uniq,',
|
||||
'count(*) as cnt',
|
||||
'from ({{sql}}) _wrap ',
|
||||
'),',
|
||||
'null_ratio as (',
|
||||
'SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ',
|
||||
'true_ratio as (',
|
||||
'SELECT sum(case when {{column}} is true then 1 else 0 end)::numeric / count(*)::numeric as true_ratio FROM ({{sql}}) _wrap) ',
|
||||
'SELECT * FROM true_ratio, null_ratio, stats'
|
||||
];
|
||||
var query = Mustache.render(s.join('\n'), {
|
||||
column: column,
|
||||
sql: sql
|
||||
});
|
||||
|
||||
this.execute(query, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = data.rows[0];
|
||||
|
||||
callback(null, {
|
||||
type: 'boolean',
|
||||
null_ratio: row.null_ratio,
|
||||
true_ratio: row.true_ratio,
|
||||
distinct: row.uniq,
|
||||
count: row.cnt
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SQL.prototype.describeGeom = function (sql, column, callback) {
|
||||
var s = [
|
||||
'with geotype as (',
|
||||
'select st_geometrytype({{column}}) as geometry_type from ({{sql}}) _w where {{column}} is not null limit 1',
|
||||
')',
|
||||
'select * from geotype'
|
||||
];
|
||||
|
||||
var query = Mustache.render(s.join('\n'), {
|
||||
column: column,
|
||||
sql: sql
|
||||
});
|
||||
function simplifyType (g) {
|
||||
return {
|
||||
'st_multipolygon': 'polygon',
|
||||
'st_polygon': 'polygon',
|
||||
'st_multilinestring': 'line',
|
||||
'st_linestring': 'line',
|
||||
'st_multipoint': 'point',
|
||||
'st_point': 'point'
|
||||
}[g.toLowerCase()];
|
||||
}
|
||||
|
||||
this.execute(query, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = data.rows[0];
|
||||
callback(null, {
|
||||
type: 'geom',
|
||||
// lon,lat -> lat, lon
|
||||
geometry_type: row.geometry_type,
|
||||
simplified_geometry_type: simplifyType(row.geometry_type)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SQL.prototype.columns = function (sql, options, callback) {
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
if (_.isFunction(fn)) {
|
||||
callback = fn;
|
||||
}
|
||||
var s = 'select * from (' + sql + ') __wrap limit 0';
|
||||
var exclude = ['cartodb_id', 'latitude', 'longitude', 'created_at', 'updated_at', 'lat', 'lon', 'the_geom_webmercator'];
|
||||
this.execute(s, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var t = {};
|
||||
for (var i in data.fields) {
|
||||
if (exclude.indexOf(i) === -1) {
|
||||
t[i] = data.fields[i].type;
|
||||
}
|
||||
}
|
||||
callback(null, t);
|
||||
});
|
||||
};
|
||||
|
||||
SQL.prototype.describeFloat = function (sql, column, callback) {
|
||||
var s = [
|
||||
'with stats as (',
|
||||
'select min({{column}}) as min,',
|
||||
'max({{column}}) as max,',
|
||||
'avg({{column}}) as avg,',
|
||||
'count(DISTINCT {{column}}) as cnt,',
|
||||
'count(distinct({{column}})) as uniq,',
|
||||
'count(*) as cnt,',
|
||||
'sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio,',
|
||||
'stddev_pop({{column}}) / count({{column}}) as stddev,',
|
||||
'CASE WHEN abs(avg({{column}})) > 1e-7 THEN stddev({{column}}) / abs(avg({{column}})) ELSE 1e12 END as stddevmean,',
|
||||
'CDB_DistType(array_agg("{{column}}"::numeric)) as dist_type ',
|
||||
'from ({{sql}}) _wrap ',
|
||||
'),',
|
||||
'params as (select min(a) as min, (max(a) - min(a)) / 7 as diff from ( select {{column}} as a from ({{sql}}) _table_sql where {{column}} is not null ) as foo ),',
|
||||
'histogram as (',
|
||||
'select array_agg(row(bucket, range, freq)) as hist from (',
|
||||
'select CASE WHEN uniq > 1 then width_bucket({{column}}, min-0.01*abs(min), max+0.01*abs(max), 100) ELSE 1 END as bucket,',
|
||||
'numrange(min({{column}})::numeric, max({{column}})::numeric) as range,',
|
||||
'count(*) as freq',
|
||||
'from ({{sql}}) _w, stats',
|
||||
'group by 1',
|
||||
'order by 1',
|
||||
') __wrap',
|
||||
'),',
|
||||
'hist as (',
|
||||
'select array_agg(row(d, c)) cat_hist from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a',
|
||||
'),',
|
||||
'buckets as (',
|
||||
'select CDB_QuantileBins(array_agg(distinct({{column}}::numeric)), 7) as quantiles, ',
|
||||
' (select array_agg(x::numeric) FROM (SELECT (min + n * diff)::numeric as x FROM generate_series(1,7) n, params) p) as equalint,',
|
||||
// ' CDB_EqualIntervalBins(array_agg({{column}}::numeric), 7) as equalint, ',
|
||||
' CDB_JenksBins(array_agg(distinct({{column}}::numeric)), 7) as jenks, ',
|
||||
' CDB_HeadsTailsBins(array_agg(distinct({{column}}::numeric)), 7) as headtails ',
|
||||
'from ({{sql}}) _table_sql where {{column}} is not null',
|
||||
')',
|
||||
'select * from histogram, stats, buckets, hist'
|
||||
];
|
||||
|
||||
var query = Mustache.render(s.join('\n'), {
|
||||
column: column,
|
||||
sql: sql
|
||||
});
|
||||
|
||||
this.execute(query, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = data.rows[0];
|
||||
var s = arrayAgg(row.hist);
|
||||
var h = arrayAgg(row.cat_hist);
|
||||
callback(null, {
|
||||
type: 'number',
|
||||
cat_hist:
|
||||
_(h).map(function (row) {
|
||||
var r = row.match(/\((.*),(\d+)/);
|
||||
return [+r[1], +r[2]];
|
||||
}),
|
||||
hist: _(s).map(function (row) {
|
||||
if (row.indexOf('empty') > -1) return;
|
||||
var els = row.split('"');
|
||||
return { index: els[0].replace(/\D/g, ''),
|
||||
range: els[1].split(',').map(function (d) { return d.replace(/\D/g, ''); }),
|
||||
freq: els[2].replace(/\D/g, '') };
|
||||
}),
|
||||
stddev: row.stddev,
|
||||
null_ratio: row.null_ratio,
|
||||
count: row.cnt,
|
||||
distinct: row.uniq,
|
||||
// lstddev: row.lstddev,
|
||||
avg: row.avg,
|
||||
max: row.max,
|
||||
min: row.min,
|
||||
stddevmean: row.stddevmean,
|
||||
weight: (row.uniq > 1 ? 1 : 0) * (1 - row.null_ratio) * (row.stddev < -1 ? 1 : (row.stddev < 1 ? 0.5 : (row.stddev < 3 ? 0.25 : 0.1))),
|
||||
quantiles: row.quantiles,
|
||||
equalint: row.equalint,
|
||||
jenks: row.jenks,
|
||||
headtails: row.headtails,
|
||||
dist_type: row.dist_type
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// describe a column
|
||||
SQL.prototype.describe = function (sql, column, options) {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
if (_.isFunction(fn)) {
|
||||
var _callback = fn;
|
||||
}
|
||||
var callback = function (err, data) {
|
||||
if (err) {
|
||||
_callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
data.column = column;
|
||||
_callback(null, data);
|
||||
};
|
||||
var s = 'select * from (' + sql + ') __wrap limit 0';
|
||||
this.execute(s, function (err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var type = (options && options.type) ? options.type : data.fields[column].type;
|
||||
|
||||
if (!type) {
|
||||
callback(new Error('column does not exist'));
|
||||
} else if (type === 'string') {
|
||||
self.describeString(sql, column, callback);
|
||||
} else if (type === 'number') {
|
||||
self.describeFloat(sql, column, callback);
|
||||
} else if (type === 'geometry') {
|
||||
self.describeGeom(sql, column, callback);
|
||||
} else if (type === 'date') {
|
||||
self.describeDate(sql, column, callback);
|
||||
} else if (type === 'boolean') {
|
||||
self.describeBoolean(sql, column, callback);
|
||||
} else {
|
||||
callback(new Error('column type is not supported'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = SQL;
|
||||
@@ -0,0 +1,47 @@
|
||||
# V4 API
|
||||
|
||||
This folder contains the source files used in the `v4 public api`.
|
||||
|
||||
This api build using wrappers over the internal objects, those wrappers have an easy-to-use public methods,
|
||||
the reference to the internal objects can be obtained with the `$getInternalModel` method.
|
||||
|
||||
The `$` before a method name is a naming convention. Those methods shall not be exposed in the public API
|
||||
but can be used from different files. (Public only for developers).
|
||||
|
||||
## Api structure
|
||||
|
||||
All the api methods and objects are exposed through the public `carto` object.
|
||||
|
||||
- `carto.client` : The main object used in a CARTO.js app
|
||||
- `carto.source`: Namespace for the sources.
|
||||
- `Dataset`: Get all the data from a table
|
||||
- `SQL`: Get the data from a custom SQL query
|
||||
- `carto.style`: Namespace for the styles
|
||||
- `CartoCSS`: Constructor to build layer styles.
|
||||
- `carto.layer` : Namespace for the layers
|
||||
- `Layer`: Constructor to build a Layer object
|
||||
- `carto.dataview` : Namespace for the dataviews
|
||||
- `Formula`: Constructor to build a Formula dataview
|
||||
- `Category`: Constructor to build a Category dataview
|
||||
- `Histogram`: Constructor to build a Histogram dataview
|
||||
- `Time Series`: Constructor to build a Time Series dataview
|
||||
- `carto.filter` : Namespace for the filters
|
||||
- `BoundingBox`: Constructor to build a BoundingBox filter
|
||||
- `BoundingBoxLeaflet`: Constructor to build a BoundingBoxLeaflet filter
|
||||
|
||||
- `carto.operation` : Enum with the operations available.
|
||||
- `carto.dataview.status` : Enum with the dataview statuses available.
|
||||
|
||||
## Usage
|
||||
|
||||
### Common.js
|
||||
|
||||
```javascript
|
||||
const carto = require('cartojs');
|
||||
```
|
||||
|
||||
### Loading CARTO.js from a CDN
|
||||
|
||||
```javascript
|
||||
window.carto; // All the api is available here
|
||||
```
|
||||
@@ -0,0 +1,521 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CartoError = require('./error-handling/carto-error');
|
||||
var Engine = require('../../engine');
|
||||
var Events = require('./events');
|
||||
var LayerBase = require('./layer/base');
|
||||
var Layers = require('./layers');
|
||||
var VERSION = require('../../../package.json').version;
|
||||
var CartoValidationError = require('./error-handling/carto-validation-error');
|
||||
var utils = require('../../core/util');
|
||||
|
||||
function getValidationError (code) {
|
||||
return new CartoValidationError('client', code);
|
||||
}
|
||||
|
||||
const DEFAULT_SERVER_URL = 'https://{username}.carto.com';
|
||||
|
||||
/**
|
||||
* This is the entry point for a CARTO.js application.
|
||||
*
|
||||
* A CARTO client allows managing layers and dataviews. Some operations like addding a layer or a dataview are asynchronous.
|
||||
* The client takes care of the communication between CARTO.js and the server for you.
|
||||
*
|
||||
* To create a new client you need a CARTO account, where you will be able to get
|
||||
* your API key and username.
|
||||
*
|
||||
* If you want to learn more about authorization and authentication, please read the authorization fundamentals section of our Developer Center.
|
||||
*
|
||||
* @param {object} settings
|
||||
* @param {string} settings.apiKey - API key used to authenticate against CARTO
|
||||
* @param {string} settings.username - Name of the user
|
||||
* @param {string} [settings.serverUrl='https://{username}.carto.com'] - URL of the windshaft server. Only needed in custom installations. Pattern: `http(s)://{username}.your.carto.instance` or `http(s)://your.carto.instance/user/{username}` (only for On-Premises environments).
|
||||
*
|
||||
* @example
|
||||
* var client = new carto.Client({
|
||||
* apiKey: 'YOUR_API_KEY_HERE',
|
||||
* username: 'YOUR_USERNAME_HERE'
|
||||
* });
|
||||
*
|
||||
* var client = new carto.Client({
|
||||
* apiKey: 'YOUR_API_KEY_HERE',
|
||||
* username: 'YOUR_USERNAME_HERE',
|
||||
* serverUrl: 'http://{username}.your.carto.instance'
|
||||
* });
|
||||
*
|
||||
* @constructor
|
||||
* @memberof carto
|
||||
* @api
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*/
|
||||
function Client (settings) {
|
||||
settings.serverUrl = (settings.serverUrl || DEFAULT_SERVER_URL).replace(/{username}/, settings.username || '');
|
||||
_checkSettings(settings);
|
||||
this._layers = new Layers();
|
||||
this._dataviews = [];
|
||||
this._engine = new Engine({
|
||||
apiKey: settings.apiKey,
|
||||
username: settings.username,
|
||||
serverUrl: settings.serverUrl,
|
||||
client: 'js-' + VERSION
|
||||
});
|
||||
this._bindEngine(this._engine);
|
||||
}
|
||||
|
||||
_.extend(Client.prototype, Backbone.Events);
|
||||
|
||||
/**
|
||||
* Add a layer to the client.
|
||||
* If the layer id already exists in the client this method will throw an error.
|
||||
*
|
||||
* @param {carto.layer.Base} - The layer to be added
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @example
|
||||
* // Add a layer to the client
|
||||
* client.addLayer(layer)
|
||||
* .then(() => {
|
||||
* console.log('Layer added');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* });
|
||||
*
|
||||
* @returns {Promise} - A promise that will be fulfilled when the layer is added
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.addLayer = function (layer) {
|
||||
return this.addLayers([layer]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add multiple layers to the client at once.
|
||||
*
|
||||
* @param {carto.layer.Base[]} - An array with the layers to be added. Note that ([A, B]) displays B as the top layer.
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @example
|
||||
* // Add multiple layers ad once layer to the client
|
||||
* client.addLayers([layer0, layer1])
|
||||
* .then(() => {
|
||||
* console.log('Layers added');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* });
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the layers are added
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.addLayers = function (layers) {
|
||||
layers.forEach(this._addLayer, this);
|
||||
return this._reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a layer from the client.
|
||||
*
|
||||
* @example
|
||||
* // Remove a layer from the client
|
||||
* client.removeLayer(layer)
|
||||
* .then(() => {
|
||||
* console.log('Layer removed');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* });
|
||||
*
|
||||
* @param {carto.layer.Base} - The layer to be removed
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the layer is removed
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.removeLayer = function (layer) {
|
||||
return this.removeLayers([layer]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove multiple layers from the client.
|
||||
*
|
||||
* @example
|
||||
* // Remove multiple layers from the client
|
||||
* client.removeLayers([layer1, layer2])
|
||||
* .then(() => {
|
||||
* console.log('Layers removed');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @param {carto.layer.Base[]} - An array with the layers to be removed
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the layers are removed
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.removeLayers = function (layers) {
|
||||
var layersToRemove = layers.slice(0);
|
||||
layersToRemove.forEach(this._removeLayer, this);
|
||||
|
||||
return this._reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Move layer order.
|
||||
*
|
||||
* @example
|
||||
* // Move layer order
|
||||
* client.moveLayer(layer1, 0)
|
||||
* .then(() => {
|
||||
* console.log('Layer moved');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @param {carto.layer.Base} - The layer to be moved
|
||||
* @param {number} toIndex - Final index for the layer
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the layer is moved
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.moveLayer = function (layer, toIndex) {
|
||||
var fromIndex = this._layers.indexOf(layer);
|
||||
this._moveLayer(layer, toIndex);
|
||||
if (fromIndex === toIndex) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this._reload();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the {@link carto.layer.Base|layers} from the client.
|
||||
*
|
||||
* @example
|
||||
* // Get all layers from the client
|
||||
* const layers = client.getLayers();
|
||||
*
|
||||
* @example
|
||||
* // Hide all layers from the client
|
||||
* client.getLayers().forEach(layer => layer.hide());
|
||||
*
|
||||
* @returns {carto.layer.Base[]} An array with all the Layers from the client
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.getLayers = function () {
|
||||
return this._layers.toArray();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a dataview to the client.
|
||||
*
|
||||
* @example
|
||||
* // Add a dataview to the client
|
||||
* client.addDataview(dataview)
|
||||
* .then(() => {
|
||||
* console.log('Dataview added');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* }):
|
||||
*
|
||||
* @param {carto.dataview.Base} - The dataview to be added
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} - A promise that will be fulfilled when the dataview is added
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.addDataview = function (dataview) {
|
||||
return this.addDataviews([dataview]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add multipe dataviews to the client.
|
||||
*
|
||||
* @example
|
||||
* // Add several dataviews to the client
|
||||
* client.addDataview([dataview0, dataview1])
|
||||
* .then(() => {
|
||||
* console.log('Dataviews added');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* }):
|
||||
*
|
||||
* @param {carto.dataview.Base[]} - An array with the dataviews to be added
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the dataviews are added
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.addDataviews = function (dataviews) {
|
||||
dataviews.forEach(this._addDataview, this);
|
||||
return this._reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a dataview from the client.
|
||||
*
|
||||
* @example
|
||||
* // Remove a dataview from the client
|
||||
* client.removeDataview(dataview)
|
||||
* .then(() => {
|
||||
* console.log('Dataviews removed');
|
||||
* })
|
||||
* .catch(cartoError => {
|
||||
* console.error(cartoError.message);
|
||||
* }):
|
||||
*
|
||||
* @param {carto.dataview.Base} - The dataview array to be removed
|
||||
*
|
||||
* @fires error
|
||||
* @fires success
|
||||
*
|
||||
* @returns {Promise} A promise that will be fulfilled when the dataview is removed
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.removeDataview = function (dataview) {
|
||||
var dataviewIndex = this._dataviews.indexOf(dataview);
|
||||
|
||||
if (dataviewIndex === -1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this._dataviews.splice(dataviewIndex, 1);
|
||||
this._engine.removeDataview(dataview.$getInternalModel());
|
||||
dataview.disable();
|
||||
return this._reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the dataviews from the client.
|
||||
*
|
||||
* @example
|
||||
* // Get all the dataviews from the client
|
||||
* const dataviews = client.getDataviews();
|
||||
*
|
||||
* @returns {carto.dataview.Base[]} An array with all the dataviews from the client
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.getDataviews = function () {
|
||||
return this._dataviews;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a {@link http://leafletjs.com/reference-1.3.1.html#tilelayer|leaflet layer} that groups all the layers that have been
|
||||
* added to this client.
|
||||
*
|
||||
* @example
|
||||
* // Get the leafletlayer from the client
|
||||
* const cartoLeafletLayer = client.getLeafletLayer();
|
||||
*
|
||||
* @example
|
||||
* // Add the leafletLayer to a leafletMap
|
||||
* client.getLeafletLayer().addTo(map);
|
||||
*
|
||||
* @param {object} options - {@link https://leafletjs.com/reference-1.3.0.html#tilelayer-minzoom|L.TileLayer} options.
|
||||
*
|
||||
* @returns A {@link http://leafletjs.com/reference-1.3.1.html#tilelayer|L.TileLayer} layer that groups all the layers.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.getLeafletLayer = function (options) {
|
||||
// Check if Leaflet is loaded
|
||||
utils.isLeafletLoaded();
|
||||
if (!this._leafletLayer) {
|
||||
var LeafletLayer = require('./native/leaflet-layer');
|
||||
this._leafletLayer = new LeafletLayer(this._layers, this._engine, options);
|
||||
}
|
||||
return this._leafletLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a {@link https://developers.google.com/maps/documentation/javascript/maptypes|google.maps.MapType} that groups all the layers that have been
|
||||
* added to this client.
|
||||
*
|
||||
* @example
|
||||
* // Get googlemaps MapType from client
|
||||
* const gmapsMapType = client.getGoogleMapsMapType();
|
||||
*
|
||||
* @example
|
||||
* // Add googlemaps MapType to a google map
|
||||
* googleMap.overlayMapTypes.push(client.getGoogleMapsMapType(googleMap));
|
||||
*
|
||||
* @param {google.maps.Map} - The native Google Maps map where the CARTO layers will be displayed.
|
||||
*
|
||||
* @return {google.maps.MapType} A Google Maps mapType that groups all the layers:
|
||||
* {@link https://developers.google.com/maps/documentation/javascript/maptypes|google.maps.MapType}
|
||||
* @api
|
||||
*/
|
||||
Client.prototype.getGoogleMapsMapType = function (map) {
|
||||
// Check if Google Maps is loaded
|
||||
utils.isGoogleMapsLoaded();
|
||||
if (!this._gmapsMapType) {
|
||||
var GoogleMapsMapType = require('./native/google-maps-map-type');
|
||||
this._gmapsMapType = new GoogleMapsMapType(this._layers, this._engine, map);
|
||||
}
|
||||
return this._gmapsMapType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call engine.reload wrapping the native cartojs errors
|
||||
* into public CartoErrors.
|
||||
*/
|
||||
Client.prototype._reload = function () {
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(function (error) {
|
||||
return Promise.reject(new CartoError(error));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper used to link a layer and an engine.
|
||||
* @private
|
||||
*/
|
||||
Client.prototype._addLayer = function (layer, engine) {
|
||||
_checkLayer(layer);
|
||||
this._checkDuplicatedLayerId(layer);
|
||||
this._layers.add(layer);
|
||||
layer.$setClient(this);
|
||||
layer.$setEngine(this._engine);
|
||||
this._engine.addLayer(layer.$getInternalModel());
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper used to remove a layer from the client.
|
||||
* @private
|
||||
*/
|
||||
Client.prototype._removeLayer = function (layer) {
|
||||
_checkLayer(layer);
|
||||
this._layers.remove(layer);
|
||||
this._engine.removeLayer(layer.$getInternalModel());
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper used to remove a layer from the client.
|
||||
* @private
|
||||
*/
|
||||
Client.prototype._moveLayer = function (layer, toIndex) {
|
||||
_checkLayer(layer);
|
||||
_checkLayerIndex(toIndex, this._layers.size());
|
||||
this._layers.move(layer, toIndex);
|
||||
this._engine.moveLayer(layer.$getInternalModel(), toIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper used to link a dataview and an engine
|
||||
* @private
|
||||
*/
|
||||
Client.prototype._addDataview = function (dataview, engine) {
|
||||
this._dataviews.push(dataview);
|
||||
dataview.$setEngine(this._engine);
|
||||
this._engine.addDataview(dataview.$getInternalModel());
|
||||
};
|
||||
|
||||
/**
|
||||
* Client exposes Event.SUCCESS and RELOAD_ERROR to the api users,
|
||||
* those events are wrappers using _engine internaly.
|
||||
*/
|
||||
Client.prototype._bindEngine = function (engine) {
|
||||
engine.on(Engine.Events.RELOAD_SUCCESS, function () {
|
||||
this.trigger(Events.SUCCESS);
|
||||
}.bind(this));
|
||||
|
||||
engine.on(Engine.Events.RELOAD_ERROR, function (err) {
|
||||
this.trigger(Events.ERROR, new CartoError(err, { layers: this._layers }));
|
||||
}.bind(this));
|
||||
|
||||
engine.on(Engine.Events.LAYER_ERROR, function (err) {
|
||||
this.trigger(Events.ERROR, new CartoError(err));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if some layer in the client has the same id.
|
||||
* @param {carto.layer.Base} layer
|
||||
*/
|
||||
Client.prototype._checkDuplicatedLayerId = function (layer) {
|
||||
if (this._layers.findById(layer.getId())) {
|
||||
throw getValidationError('duplicatedLayerId');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to reduce duplicated code.
|
||||
*/
|
||||
function _checkLayer (layer) {
|
||||
if (!(layer instanceof LayerBase)) {
|
||||
throw getValidationError('badLayerType');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkLayerIndex (index, size) {
|
||||
if (!_.isNumber(index)) {
|
||||
throw getValidationError('indexNumber');
|
||||
}
|
||||
if (index < 0 || index >= size) {
|
||||
throw getValidationError('indexOutOfRange');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkSettings (settings) {
|
||||
_checkApiKey(settings.apiKey);
|
||||
_checkUsername(settings.username);
|
||||
if (settings.serverUrl) {
|
||||
_checkServerUrl(settings.serverUrl, settings.username);
|
||||
}
|
||||
}
|
||||
|
||||
function _checkApiKey (apiKey) {
|
||||
if (!apiKey) {
|
||||
throw getValidationError('apiKeyRequired');
|
||||
}
|
||||
if (!_.isString(apiKey)) {
|
||||
throw getValidationError('apiKeyString');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkUsername (username) {
|
||||
if (!username) {
|
||||
throw getValidationError('usernameRequired');
|
||||
}
|
||||
if (!_.isString(username)) {
|
||||
throw getValidationError('usernameString');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkServerUrl (serverUrl, username) {
|
||||
var urlregex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
|
||||
if (!serverUrl.match(urlregex)) {
|
||||
throw getValidationError('nonValidServerURL');
|
||||
}
|
||||
if (serverUrl.indexOf(username) < 0) {
|
||||
throw getValidationError('serverURLDoesntMatchUsername');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
@@ -0,0 +1,106 @@
|
||||
var _ = require('underscore');
|
||||
/**
|
||||
* Constants module for dataviews
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum for operation values.
|
||||
*
|
||||
* @enum {string} carto.operation
|
||||
* @readonly
|
||||
* @memberof carto
|
||||
* @api
|
||||
*/
|
||||
var operation = {
|
||||
/** Number of elements */
|
||||
COUNT: 'count',
|
||||
/** Sum */
|
||||
SUM: 'sum',
|
||||
/** Average */
|
||||
AVG: 'avg',
|
||||
/** Maximum */
|
||||
MAX: 'max',
|
||||
/** Minimum */
|
||||
MIN: 'min'
|
||||
};
|
||||
|
||||
function isValidOperation (op) {
|
||||
return _.contains(operation, op);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for dataview status values.
|
||||
*
|
||||
* @enum {string} carto.dataview.status
|
||||
* @readonly
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
*/
|
||||
var status = {
|
||||
/** Not fetched with the server */
|
||||
NOT_LOADED: 'notLoaded',
|
||||
/** Fetching with the server */
|
||||
LOADING: 'loading',
|
||||
/** Fetch completed */
|
||||
LOADED: 'loaded',
|
||||
/** Error in fetch */
|
||||
ERROR: 'error'
|
||||
};
|
||||
|
||||
/**
|
||||
* Enum for dataview time aggregations.
|
||||
*
|
||||
* @enum {string} carto.dataview.timeAggregation
|
||||
* @readonly
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
*/
|
||||
var timeAggregation = {
|
||||
/** Auto */
|
||||
AUTO: 'auto',
|
||||
/** Millennium */
|
||||
MILLENNIUM: 'millennium',
|
||||
/** Century */
|
||||
CENTURY: 'century',
|
||||
/** Decade */
|
||||
DECADE: 'decade',
|
||||
/** Year */
|
||||
YEAR: 'year',
|
||||
/** Quarter */
|
||||
QUARTER: 'quarter',
|
||||
/** Month */
|
||||
MONTH: 'month',
|
||||
/** Week */
|
||||
WEEK: 'week',
|
||||
/** Day */
|
||||
DAY: 'day',
|
||||
/** Hour */
|
||||
HOUR: 'hour',
|
||||
/** Minute */
|
||||
MINUTE: 'minute'
|
||||
};
|
||||
|
||||
function isValidTimeAggregation (agg) {
|
||||
return _.contains(timeAggregation, agg);
|
||||
}
|
||||
|
||||
/**
|
||||
* ATTRIBUTION constant
|
||||
*
|
||||
* © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attribution">CARTO</a>
|
||||
*
|
||||
* @type {string}
|
||||
* @constant
|
||||
* @memberof carto
|
||||
* @api
|
||||
*/
|
||||
var ATTRIBUTION = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attribution">CARTO</a>';
|
||||
|
||||
module.exports = {
|
||||
operation: operation,
|
||||
status: status,
|
||||
timeAggregation: timeAggregation,
|
||||
isValidOperation: isValidOperation,
|
||||
isValidTimeAggregation: isValidTimeAggregation,
|
||||
ATTRIBUTION: ATTRIBUTION
|
||||
};
|
||||
@@ -0,0 +1,490 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var status = require('../constants').status;
|
||||
var SourceBase = require('../source/base');
|
||||
var FilterBase = require('../filter/base');
|
||||
var SQLFilterBase = require('../filter/base-sql');
|
||||
var SpatialFilterTypes = require('../filter/spatial-filter-types');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
|
||||
/**
|
||||
* Base class for dataview objects.
|
||||
*
|
||||
* Dataviews are a way to extract data from a CARTO account in predefined ways
|
||||
* (eg: a list of categories, the result of a formula operation, etc.).
|
||||
*
|
||||
* **This object should not be used directly**
|
||||
*
|
||||
* The data used in a dataviews cames from a {@link carto.source.Base|source} that might change
|
||||
* due to different reasons (eg: SQL query changed).
|
||||
*
|
||||
* When dataview data changes the dataview will trigger events to notify subscribers when new data is available.
|
||||
*
|
||||
* @example
|
||||
* // Keep your widget data sync. Remember each dataview has his own data format.
|
||||
* dataview.on('dataChanged', newData => {
|
||||
* renderWidget(newData);
|
||||
* })
|
||||
*
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @memberof carto.dataview
|
||||
* @fires dataChanged
|
||||
* @fires columnChanged
|
||||
* @fires statusChanged
|
||||
* @fires error
|
||||
* @api
|
||||
*/
|
||||
function Base () { }
|
||||
|
||||
_.extend(Base.prototype, Backbone.Events);
|
||||
|
||||
/**
|
||||
* Return the current dataview status.
|
||||
*
|
||||
* @return {carto.dataview.status} Current dataview status
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getStatus = function () {
|
||||
return this._status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true is the current status is loading.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.isLoading = function () {
|
||||
return this._status === status.LOADING;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true is the current status is loaded.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.isLoaded = function () {
|
||||
return this._status === status.LOADED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true is the current status is error.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.hasError = function () {
|
||||
return this._status === status.ERROR;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the dataview. When enabled, a dataview fetches new data
|
||||
* when the map changes (changing map configuration or changing map
|
||||
* bounding box).
|
||||
*
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.enable = function () {
|
||||
return this._setEnabled(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable the dataview. This stops the dataview from fetching new
|
||||
* data when there is a map change (like changing map configuration or changing map
|
||||
* bounding box).
|
||||
*
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.disable = function () {
|
||||
return this._setEnabled(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the dataview is enabled.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.isEnabled = function () {
|
||||
return this._enabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current source where the dataview gets the data from.
|
||||
*
|
||||
* @return {carto.source.Base} Current source object
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getSource = function () {
|
||||
return this._source;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the dataview column.
|
||||
*
|
||||
* @param {string} column
|
||||
* @fires columnChanged
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.setColumn = function (column) {
|
||||
this._checkColumn(column);
|
||||
this._column = column;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set('column', this._column);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current dataview column where the dataview is applied.
|
||||
*
|
||||
* @return {string} Current dataview column
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getColumn = function () {
|
||||
return this._column;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a {@link carto.filter.Base|filter}.
|
||||
*
|
||||
* @param {carto.filter.Base} filter
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.addFilter = function (filter) {
|
||||
this._checkFilter(filter);
|
||||
this._addSpatialFilter(filter);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a {@link carto.filter.Base|filter}.
|
||||
*
|
||||
* @param {carto.filter.Base} filter
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.removeFilter = function (filter) {
|
||||
this._checkFilter(filter);
|
||||
this._removeSpatialFilter(filter);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a {@link carto.filter.Base|filter} exists in the dataview.
|
||||
*
|
||||
* @param {carto.filter.Base} filter
|
||||
* @return {carto.dataview.Base} this
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.hasFilter = function (filter) {
|
||||
this._checkFilter(filter);
|
||||
var hasBBoxFilter = (filter === this._boundingBoxFilter) &&
|
||||
(this._internalModel && this._internalModel.get('sync_on_bbox_change'));
|
||||
|
||||
var hasCircleFilter = (filter === this._circleFilter) &&
|
||||
(this._internalModel && this._internalModel.get('sync_on_circle_change'));
|
||||
|
||||
var hasPolygonFilter = (filter === this._polygonFilter) &&
|
||||
(this._internalModel && this._internalModel.get('sync_on_polygon_change'));
|
||||
|
||||
return hasBBoxFilter || hasCircleFilter || hasPolygonFilter;
|
||||
};
|
||||
|
||||
Base.prototype.getData = function () {
|
||||
throw new Error('getData must be implemented by the particular dataview.');
|
||||
};
|
||||
|
||||
// Protected methods
|
||||
|
||||
Base.prototype.DEFAULTS = {};
|
||||
|
||||
/**
|
||||
* Initialize dataview.
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the dataview will fetch the data
|
||||
* @param {string} column - The column name to get the data
|
||||
* @param {object} options - It depends on the instance
|
||||
*/
|
||||
Base.prototype._initialize = function (source, column, options) {
|
||||
options = _.defaults(options || {}, this.DEFAULTS);
|
||||
|
||||
this._checkSource(source);
|
||||
this._checkColumn(column);
|
||||
this._checkOptions(options);
|
||||
|
||||
this._source = source;
|
||||
this._column = column;
|
||||
this._options = options;
|
||||
|
||||
this._status = status.NOT_LOADED;
|
||||
this._enabled = true;
|
||||
this._boundingBoxFilter = null;
|
||||
};
|
||||
|
||||
Base.prototype._checkSource = function (source) {
|
||||
if (!(source instanceof SourceBase)) {
|
||||
throw this._getValidationError('sourceRequired');
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._checkColumn = function (column) {
|
||||
if (_.isUndefined(column)) {
|
||||
throw this._getValidationError('columnRequired');
|
||||
}
|
||||
if (!_.isString(column)) {
|
||||
throw this._getValidationError('columnString');
|
||||
}
|
||||
if (_.isEmpty(column)) {
|
||||
throw this._getValidationError('emptyColumn');
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._checkOptions = function (options) {
|
||||
throw new Error('_checkOptions must be implemented by the particular dataview.');
|
||||
};
|
||||
|
||||
Base.prototype._checkFilter = function (filter) {
|
||||
if (!(filter instanceof FilterBase) || filter instanceof SQLFilterBase) {
|
||||
throw this._getValidationError('filterRequired');
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._createInternalModel = function (engine) {
|
||||
throw new Error('_createInternalModel must be implemented by the particular dataview.');
|
||||
};
|
||||
|
||||
Base.prototype._setEnabled = function (enabled) {
|
||||
this._enabled = enabled;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set('enabled', enabled);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Base.prototype._listenToInternalModelSharedEvents = function () {
|
||||
if (this._internalModel) {
|
||||
this.listenTo(this._internalModel, 'change:data', this._onDataChanged);
|
||||
this.listenTo(this._internalModel, 'change:column', this._onColumnChanged);
|
||||
this.listenTo(this._internalModel, 'loading', this._onStatusLoading);
|
||||
this.listenTo(this._internalModel, 'loaded', this._onStatusLoaded);
|
||||
this.listenTo(this._internalModel, 'statusError', this._onStatusError);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._onDataChanged = function () {
|
||||
this.trigger('dataChanged', this.getData());
|
||||
};
|
||||
|
||||
Base.prototype._onColumnChanged = function () {
|
||||
if (this._internalModel) {
|
||||
this._column = this._internalModel.get('column');
|
||||
}
|
||||
this.trigger('columnChanged', this._column);
|
||||
};
|
||||
|
||||
Base.prototype._onStatusLoading = function () {
|
||||
this._status = status.LOADING;
|
||||
this.trigger('statusChanged', this._status);
|
||||
};
|
||||
|
||||
Base.prototype._onStatusLoaded = function () {
|
||||
this._status = status.LOADED;
|
||||
this.trigger('statusChanged', this._status);
|
||||
};
|
||||
|
||||
Base.prototype._onStatusError = function (model, error) {
|
||||
this._status = status.ERROR;
|
||||
this.trigger('statusChanged', this._status, error);
|
||||
this._triggerError(this, error);
|
||||
};
|
||||
|
||||
Base.prototype._changeProperty = function (key, value, internalKey) {
|
||||
var prevValue = this['_' + key];
|
||||
this['_' + key] = value;
|
||||
if (prevValue === value) {
|
||||
return;
|
||||
}
|
||||
this._triggerChange(key, value);
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set(internalKey || key, value);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._changeProperties = function (properties) {
|
||||
_.each(properties, (value, key) => {
|
||||
const prevValue = this[`_${key}`];
|
||||
|
||||
if (prevValue !== value) {
|
||||
this[`_${key}`] = value;
|
||||
this._triggerChange(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set(properties);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._triggerChange = function (key, value) {
|
||||
this.trigger(key + 'Changed', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire a CartoError event from a internalDataviewError.
|
||||
*/
|
||||
Base.prototype._triggerError = function (model, internalDataviewError) {
|
||||
this.trigger('error', new CartoError(internalDataviewError));
|
||||
};
|
||||
|
||||
Base.prototype._addSpatialFilter = function (spatialFilter) {
|
||||
switch (spatialFilter.type) {
|
||||
case SpatialFilterTypes.BBOX:
|
||||
this._addBoundingBoxFilter(spatialFilter);
|
||||
break;
|
||||
case SpatialFilterTypes.CIRCLE:
|
||||
this._addCircleFilter(spatialFilter);
|
||||
break;
|
||||
case SpatialFilterTypes.POLYGON:
|
||||
this._addPolygonFilter(spatialFilter);
|
||||
break;
|
||||
default:
|
||||
throw new Error('The filter is not a valid spatial filter.');
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._removeSpatialFilter = function (spatialFilter) {
|
||||
switch (spatialFilter.type) {
|
||||
case SpatialFilterTypes.BBOX:
|
||||
if (spatialFilter === this._boundingBoxFilter) {
|
||||
this._removeBoundingBoxFilter();
|
||||
}
|
||||
break;
|
||||
case SpatialFilterTypes.CIRCLE:
|
||||
if (spatialFilter === this._circleFilter) {
|
||||
this._removeCircleFilter();
|
||||
}
|
||||
break;
|
||||
case SpatialFilterTypes.POLYGON:
|
||||
if (spatialFilter === this._polygonFilter) {
|
||||
this._removePolygonFilter();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('The filter is not a valid spatial filter.');
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._addBoundingBoxFilter = function (bboxFilter) {
|
||||
if (bboxFilter === this._boundingBoxFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._boundingBoxFilter = bboxFilter;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.addBBoxFilter(this._boundingBoxFilter.$getInternalModel());
|
||||
this._internalModel.set('sync_on_bbox_change', true);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._removeBoundingBoxFilter = function () {
|
||||
this._boundingBoxFilter = null;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.removeBBoxFilter();
|
||||
this._internalModel.set('sync_on_bbox_change', false);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._addCircleFilter = function (circleFilter) {
|
||||
if (circleFilter === this._circleFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._circleFilter = circleFilter;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.addCircleFilter(this._circleFilter.$getInternalModel());
|
||||
this._internalModel.set('sync_on_circle_change', true);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._removeCircleFilter = function () {
|
||||
this._circleFilter = null;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.removeCircleFilter();
|
||||
this._internalModel.set('sync_on_circle_change', false);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._addPolygonFilter = function (polygonFilter) {
|
||||
if (polygonFilter === this._polygonFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polygonFilter = polygonFilter;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.addPolygonFilter(this._polygonFilter.$getInternalModel());
|
||||
this._internalModel.set('sync_on_polygon_change', true);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._removePolygonFilter = function () {
|
||||
this._polygonFilter = null;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.removePolygonFilter();
|
||||
this._internalModel.set('sync_on_polygon_change', false);
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype._getValidationError = function (code) {
|
||||
return new CartoValidationError('dataview', code);
|
||||
};
|
||||
|
||||
// Internal public methods
|
||||
|
||||
Base.prototype.$setEngine = function (engine) {
|
||||
this._source.$setEngine(engine);
|
||||
if (!this._internalModel) {
|
||||
this._createInternalModel(engine);
|
||||
this._listenToInternalModelSharedEvents();
|
||||
}
|
||||
};
|
||||
|
||||
Base.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
|
||||
/**
|
||||
* Fired when the column name has changed. Handler gets a parameter with the new column name.
|
||||
*
|
||||
* @event columnChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when the status has changed. Handler gets a parameter with the new status.
|
||||
*
|
||||
* Contains a single argument with the new status.
|
||||
*
|
||||
* @event statusChanged
|
||||
* @type {carto.dataview.status}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when the data has changed. Handler gets an object with specific data for the type
|
||||
* of dataview that triggered the event.
|
||||
*
|
||||
* @event dataChanged
|
||||
* @type {carto.dataview.CategoryData|carto.dataview.FormulaData|carto.dataview.HistogramData|carto.dataview.TimeSeriesData}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,268 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('../base');
|
||||
var constants = require('../../constants');
|
||||
var CategoryDataviewModel = require('../../../../dataviews/category-dataview-model');
|
||||
var CategoryFilter = require('../../../../windshaft/filters/category');
|
||||
var parseCategoryData = require('./parse-data.js');
|
||||
|
||||
/**
|
||||
*
|
||||
* A category dataview is used to aggregate data performing a operation.
|
||||
*
|
||||
* This is similar to a group by SQL operation, for example:
|
||||
*
|
||||
* ```
|
||||
* SELECT country, AVG(population) GROUP BY country
|
||||
* ```
|
||||
* The following code is the CARTO.js equivalent:
|
||||
*
|
||||
* ```javascript
|
||||
* var categoryDataview = new carto.dataview.Category(citiesSource, 'country', {
|
||||
* operation: carto.operation.AVG, // Compute the average
|
||||
* operationColumn: 'population' // The name of the column where the operation will be applied.
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Like every other dataview, this is an async object and you must wait for the data to be available.
|
||||
*
|
||||
* The data format for the category-dataview is described in {@link carto.dataview.CategoryData}
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the dataview will fetch the data
|
||||
* @param {string} column - The name of the column used to create categories
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.limit=6] - The maximum number of categories in the response
|
||||
* @param {carto.operation} [options.operation] - The operation to apply to the data
|
||||
* @param {string} [options.operationColumn] - The column where the operation will be applied
|
||||
*
|
||||
* @fires dataChanged
|
||||
* @fires columnChanged
|
||||
* @fires statusChanged
|
||||
* @fires error
|
||||
*
|
||||
* @fires limitChanged
|
||||
* @fires operationChanged
|
||||
* @fires operationColumnChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.dataview.Base
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
* @example
|
||||
* // From a cities dataset with name, country and population show the average city population per country:
|
||||
* var column = 'country'; // Aggregate the data by country.
|
||||
* var categoryDataview = new carto.dataview.Category(citiesSource, column, {
|
||||
* operation: carto.operation.AVG, // Compute the average
|
||||
* operationColumn: 'population' // The name of the column where the operation will be applied.
|
||||
* });
|
||||
* @example
|
||||
* // Listen for data updates
|
||||
* categoryDataview.on('dataChanged', newData => {
|
||||
* console.log(newData); // CategoryData object
|
||||
* });
|
||||
* @example
|
||||
* // You can listen to multiple events emmited by the category-dataview.
|
||||
* categoryDataview.on('statusChanged', (newData, error) => { });
|
||||
* categoryDataview.on('error', cartoError => { });
|
||||
*
|
||||
* // Listen to specific category-dataview events.
|
||||
* categoryDataview.on('columnChanged', newData => {
|
||||
* console.log(newData); // 'population'
|
||||
* });
|
||||
* categoryDataview.on('limitChanged', newData => {
|
||||
* console.log(newData); // 11
|
||||
* });
|
||||
* categoryDataview.on('operationChanged', newData => { });
|
||||
* categoryDataview.on('operationColumnChanged', newData => { });
|
||||
*/
|
||||
function Category (source, column, options) {
|
||||
this.DEFAULTS.operationColumn = column;
|
||||
|
||||
this._initialize(source, column, options);
|
||||
this._limit = this._options.limit;
|
||||
this._operation = this._options.operation;
|
||||
this._operationColumn = this._options.operationColumn;
|
||||
}
|
||||
|
||||
Category.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set the categories limit.
|
||||
*
|
||||
* @param {number} limit
|
||||
* @fires limitChanged
|
||||
* @return {carto.dataview.Category} this
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.setLimit = function (limit) {
|
||||
this._checkLimit(limit);
|
||||
this._changeProperty('limit', limit, 'categories');
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current categories limit.
|
||||
*
|
||||
* @return {number} Current dataview limit
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.getLimit = function () {
|
||||
return this._limit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the dataview operation.
|
||||
*
|
||||
* @param {carto.operation} operation
|
||||
* @fires operationChanged
|
||||
* @return {carto.dataview.Category} this
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.setOperation = function (operation) {
|
||||
this._checkOperation(operation);
|
||||
this._changeProperty('operation', operation, 'aggregation');
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current dataview operation.
|
||||
*
|
||||
* @return {carto.operation} Current dataview operation
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.getOperation = function () {
|
||||
return this._operation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the dataview operationColumn.
|
||||
*
|
||||
* @param {string} operationColumn
|
||||
* @fires operationColumnChanged
|
||||
* @return {carto.dataview.Category} this
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.setOperationColumn = function (operationColumn) {
|
||||
this._checkOperationColumn(operationColumn);
|
||||
this._changeProperty('operationColumn', operationColumn, 'aggregation_column');
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current dataview operationColumn.
|
||||
*
|
||||
* @return {string} Current dataview operationColumn
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.getOperationColumn = function () {
|
||||
return this._operationColumn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the resulting data.
|
||||
*
|
||||
* @return {carto.dataview.CategoryData}
|
||||
* @api
|
||||
*/
|
||||
Category.prototype.getData = function () {
|
||||
if (this._internalModel) {
|
||||
return parseCategoryData(
|
||||
this._internalModel.get('data'),
|
||||
this._internalModel.get('count'),
|
||||
this._internalModel.get('max'),
|
||||
this._internalModel.get('min'),
|
||||
this._internalModel.get('nulls'),
|
||||
this._operation
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Category.prototype.DEFAULTS = {
|
||||
limit: 6,
|
||||
operation: constants.operation.COUNT
|
||||
};
|
||||
|
||||
Category.prototype._checkOptions = function (options) {
|
||||
if (_.isUndefined(options)) {
|
||||
throw this._getValidationError('categoryOptionsRequired');
|
||||
}
|
||||
this._checkLimit(options.limit);
|
||||
this._checkOperation(options.operation);
|
||||
this._checkOperationColumn(options.operationColumn);
|
||||
};
|
||||
|
||||
Category.prototype._checkLimit = function (limit) {
|
||||
if (_.isUndefined(limit)) {
|
||||
throw this._getValidationError('categoryLimitRequired');
|
||||
}
|
||||
if (!_.isNumber(limit)) {
|
||||
throw this._getValidationError('categoryLimitNumber');
|
||||
}
|
||||
if (limit <= 0) {
|
||||
throw this._getValidationError('categoryLimitPositive');
|
||||
}
|
||||
};
|
||||
|
||||
Category.prototype._checkOperation = function (operation) {
|
||||
if (_.isUndefined(operation) || !constants.isValidOperation(operation)) {
|
||||
throw this._getValidationError('categoryInvalidOperation');
|
||||
}
|
||||
};
|
||||
|
||||
Category.prototype._checkOperationColumn = function (operationColumn) {
|
||||
if (_.isUndefined(operationColumn)) {
|
||||
throw this._getValidationError('categoryOperationRequired');
|
||||
}
|
||||
if (!_.isString(operationColumn)) {
|
||||
throw this._getValidationError('categoryOperationString');
|
||||
}
|
||||
if (_.isEmpty(operationColumn)) {
|
||||
throw this._getValidationError('categoryOperationEmpty');
|
||||
}
|
||||
};
|
||||
|
||||
Category.prototype._createInternalModel = function (engine) {
|
||||
this._internalModel = new CategoryDataviewModel({
|
||||
source: this._source.$getInternalModel(),
|
||||
column: this._column,
|
||||
aggregation: this._operation,
|
||||
aggregation_column: this._operationColumn,
|
||||
categories: this._limit,
|
||||
sync_on_bbox_change: !!this._boundingBoxFilter,
|
||||
sync_on_circle_change: !!this._circleFilter,
|
||||
sync_on_polygon_change: !!this._polygonFilter,
|
||||
enabled: this._enabled
|
||||
}, {
|
||||
engine: engine,
|
||||
filter: new CategoryFilter(),
|
||||
bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(),
|
||||
circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(),
|
||||
polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel()
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Category;
|
||||
|
||||
/**
|
||||
* Fired when limit has changed. Handler gets a parameter with the new limit.
|
||||
*
|
||||
* @event limitChanged
|
||||
* @type {number}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when operation has changed. Handler gets a parameter with the new limit.
|
||||
*
|
||||
* @event operationChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when operationColumn has changed. Handler gets a parameter with the new operationColumn.
|
||||
*
|
||||
* @event operationColumnChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,60 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Transform the data obtained from an internal category dataview into a
|
||||
* public object.
|
||||
*
|
||||
* @param {object[]} data
|
||||
* @param {number} count
|
||||
* @param {number} max
|
||||
* @param {number} min
|
||||
* @param {number} nulls
|
||||
* @param {string} operation
|
||||
*
|
||||
* @return {carto.dataview.CategoryData} - The parsed and formatted data for the given parameters
|
||||
*/
|
||||
function parseCategoryData (data, count, max, min, nulls, operation) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* @typedef {object} carto.dataview.CategoryData
|
||||
* @property {number} count - The total number of categories
|
||||
* @property {number} max - Maximum category value
|
||||
* @property {number} min - Minimum category value
|
||||
* @property {number} nulls - Number of null categories
|
||||
* @property {string} operation - Operation used
|
||||
* @property {carto.dataview.CategoryItem[]} categories
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
count: count,
|
||||
max: max,
|
||||
min: min,
|
||||
nulls: nulls,
|
||||
operation: operation,
|
||||
categories: _createCategories(data)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the histogram raw data into {@link carto.dataview.CategoryItem}
|
||||
*/
|
||||
function _createCategories (data) {
|
||||
return _.map(data, function (item) {
|
||||
/**
|
||||
* @typedef {object} carto.dataview.CategoryItem
|
||||
* @property {boolean} group - Category is a group
|
||||
* @property {string} name - Category name
|
||||
* @property {number} value - Category value
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
group: item.agg,
|
||||
name: item.name,
|
||||
value: item.value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = parseCategoryData;
|
||||
@@ -0,0 +1,134 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('../base');
|
||||
var constants = require('../../constants');
|
||||
var FormulaDataviewModel = require('../../../../dataviews/formula-dataview-model');
|
||||
var parseFormulaData = require('./parse-data.js');
|
||||
|
||||
/**
|
||||
* A formula is a simple numeric {@link carto.operation|operation} applied to the column of a {@link carto.source.Base|data source} (dataset or sql query).
|
||||
*
|
||||
* Like all dataviews, it is an async object so you must wait for the data to be available.
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the dataview will fetch the data from
|
||||
* @param {string} column - The operation will be performed using this column
|
||||
* @param {object} [options]
|
||||
* @param {carto.operation} [options.operation] - The operation to apply to the data
|
||||
*
|
||||
* @fires dataChanged
|
||||
* @fires columnChanged
|
||||
* @fires statusChanged
|
||||
* @fires error
|
||||
*
|
||||
* @fires operationChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.dataview.Base
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
* @example
|
||||
* // Given a cities dataset get the most populated city
|
||||
* var formulaDataview = new carto.dataview.Formula(citiesSource, 'population', {
|
||||
* operation: carto.operation.MAX,
|
||||
* });
|
||||
* @example
|
||||
* // You can listen to multiple events emitted by a formula dataview.
|
||||
* // Data and status are fired by all dataviews.
|
||||
* formulaDataview.on('dataChanged', newData => { });
|
||||
* formulaDataview.on('statusChanged', (newData, error) => { });
|
||||
* formulaDataview.on('error', cartoError => { });
|
||||
*
|
||||
* // Listen to specific formula-dataview events
|
||||
* formulaDataview.on('columnChanged', newData => { });
|
||||
* formulaDataview.on('operationChanged', newData => { });
|
||||
*/
|
||||
function Formula (source, column, options) {
|
||||
this._initialize(source, column, options);
|
||||
this._operation = this._options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set the dataview operation.
|
||||
*
|
||||
* @param {carto.operation} operation
|
||||
* @fires operationChanged
|
||||
* @return {carto.dataview.Formula} this
|
||||
* @api
|
||||
*/
|
||||
Formula.prototype.setOperation = function (operation) {
|
||||
this._checkOperation(operation);
|
||||
this._changeProperty('operation', operation);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current dataview operation.
|
||||
*
|
||||
* @return {carto.operation} Current dataview operation
|
||||
* @api
|
||||
*/
|
||||
Formula.prototype.getOperation = function () {
|
||||
return this._operation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the resulting data.
|
||||
*
|
||||
* @return {carto.dataview.FormulaData}
|
||||
* @api
|
||||
*/
|
||||
Formula.prototype.getData = function () {
|
||||
if (this._internalModel) {
|
||||
return parseFormulaData(
|
||||
this._internalModel.get('nulls'),
|
||||
this._operation,
|
||||
this._internalModel.get('data')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Formula.prototype.DEFAULTS = {
|
||||
operation: constants.operation.COUNT
|
||||
};
|
||||
|
||||
Formula.prototype._checkOptions = function (options) {
|
||||
if (_.isUndefined(options)) {
|
||||
throw this._getValidationError('formulaOptionsRequired');
|
||||
}
|
||||
this._checkOperation(options.operation);
|
||||
};
|
||||
|
||||
Formula.prototype._checkOperation = function (operation) {
|
||||
if (_.isUndefined(operation) || !constants.isValidOperation(operation)) {
|
||||
throw this._getValidationError('formulaInvalidOperation');
|
||||
}
|
||||
};
|
||||
|
||||
Formula.prototype._createInternalModel = function (engine) {
|
||||
this._internalModel = new FormulaDataviewModel({
|
||||
source: this._source.$getInternalModel(),
|
||||
column: this._column,
|
||||
operation: this._operation,
|
||||
sync_on_bbox_change: !!this._boundingBoxFilter,
|
||||
sync_on_circle_change: !!this._circleFilter,
|
||||
sync_on_polygon_change: !!this._polygonFilter,
|
||||
enabled: this._enabled
|
||||
}, {
|
||||
engine: engine,
|
||||
bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(),
|
||||
circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(),
|
||||
polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel()
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
/**
|
||||
* Fired when the operation has changed. Handler gets a parameter with the new operation.
|
||||
*
|
||||
* @event operationChanged
|
||||
* @type {carto.operation}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Transform the data obtained from an internal formula dataview into a
|
||||
* public object.
|
||||
*
|
||||
* @param {number} nulls
|
||||
* @param {string} operation
|
||||
* @param {number} result
|
||||
*
|
||||
* @return {carto.dataview.FormulaData} - The parsed and formatted data for the given parameters
|
||||
*/
|
||||
function parseFormulaData (nulls, operation, result) {
|
||||
/**
|
||||
* @description
|
||||
* Object containing formula data
|
||||
*
|
||||
* @typedef {object} carto.dataview.FormulaData
|
||||
* @property {number} nulls - Number of null values in the column
|
||||
* @property {string} operation - Operation used
|
||||
* @property {number} result - Result of the operation
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
nulls: nulls,
|
||||
operation: operation,
|
||||
result: result
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = parseFormulaData;
|
||||
@@ -0,0 +1,228 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('../base');
|
||||
var HistogramDataviewModel = require('../../../../dataviews/histogram-dataview-model');
|
||||
var parseHistogramData = require('./parse-data.js');
|
||||
|
||||
/**
|
||||
* A histogram is used to represent the distribution of numerical data.
|
||||
*
|
||||
* See {@link https://en.wikipedia.org/wiki/Histogram}.
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the dataview will fetch the data
|
||||
* @param {string} column - The column name to get the data
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.bins=10] - Number of bins to aggregate the data range into
|
||||
* @param {number} [options.start] - Lower limit of the data range, if not present, the lower limit of the actual data will be used. Start and end values must be used together.
|
||||
* @param {number} [options.end] - Upper limit of the data range, if not present, the upper limit of the actual data will be used. Start and end values must be used together.
|
||||
*
|
||||
* @fires dataChanged
|
||||
* @fires columnChanged
|
||||
* @fires statusChanged
|
||||
* @fires error
|
||||
*
|
||||
* @fires binsChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.dataview.Base
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
* @example
|
||||
* // Create a cities population histogram.
|
||||
* var histogram = new carto.dataview.Histogram(citiesSource, 'population');
|
||||
* // Set up a callback to render the histogram data every time new data is obtained.
|
||||
* histogram.on('dataChanged', renderData);
|
||||
* // Add the histogram to the client
|
||||
* client.addDataview(histogram);
|
||||
* @example
|
||||
* // Create a cities population histogram with only 4 bins
|
||||
* var histogram = new carto.dataview.Histogram(citiesSource, 'population', {bins: 4});
|
||||
* // Add a bounding box filter, so the data will change when the map is moved.
|
||||
* var bboxFilter = new carto.filter.BoundingBoxLeaflet(map);
|
||||
* // Set up a callback to render the histogram data every time new data is obtained.
|
||||
* histogram.on('dataChanged', histogramData => {
|
||||
* console.log(histogramData);
|
||||
* });
|
||||
* // Add the histogram to the client
|
||||
* client.addDataview(histogram);
|
||||
* @example
|
||||
* // Create a cities population histogram with a range
|
||||
* var histogram = new carto.dataview.Histogram(citiesSource, 'population', { start: 100000, end: 5000000 });
|
||||
* // Set up a callback to render the histogram data every time new data is obtained.
|
||||
* histogram.on('dataChanged', histogramData => {
|
||||
* console.log(histogramData);
|
||||
* });
|
||||
* // Add the histogram to the client
|
||||
* client.addDataview(histogram);
|
||||
* @example
|
||||
* // The histogram is an async object so it can be on different states: LOADING, ERROR...
|
||||
* // Listen to state events
|
||||
* histogram.on('statusChanged', (newStatus, error) => { });
|
||||
* // Listen to histogram errors
|
||||
* histogram.on('error', error => { });
|
||||
*/
|
||||
function Histogram (source, column, options) {
|
||||
this._initialize(source, column, options);
|
||||
this._bins = this._options.bins;
|
||||
this._start = this._options.start;
|
||||
this._end = this._options.end;
|
||||
}
|
||||
|
||||
Histogram.prototype = Object.create(Base.prototype);
|
||||
|
||||
Histogram.prototype.DEFAULTS = {
|
||||
bins: 10
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the resulting data.
|
||||
*
|
||||
* @return {carto.dataview.HistogramData}
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.getData = function () {
|
||||
if (this._internalModel) {
|
||||
return parseHistogramData(
|
||||
this._internalModel.get('data'),
|
||||
this._internalModel.get('nulls'),
|
||||
this._internalModel.get('totalAmount')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Histogram.prototype.setColumn = function (column) {
|
||||
Base.prototype.setColumn.apply(this, arguments);
|
||||
this._start = null;
|
||||
this._end = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the number of bins.
|
||||
*
|
||||
* @param {number} bins
|
||||
* @fires binsChanged
|
||||
* @return {carto.dataview.Histogram} this
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.setBins = function (bins) {
|
||||
this._validateBins(bins);
|
||||
this._changeProperty('bins', bins);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current number of bins.
|
||||
*
|
||||
* @return {number} Current number of bins
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.getBins = function () {
|
||||
return this._bins;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the lower and upper limit of the bins range
|
||||
*
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @return {carto.dataview.Histogram} this
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.setStartEnd = function (start, end) {
|
||||
this._validateStartEnd(start, end);
|
||||
|
||||
this._changeProperties({ start, end });
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the lower limit of the bins' range
|
||||
*
|
||||
* @return {number} Current value of start
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.getStart = function () {
|
||||
return this._start || this._internalModel.get('start');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the upper limit of the bins' range
|
||||
*
|
||||
* @return {number} Current value of end
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.getEnd = function () {
|
||||
return this._end || this._internalModel.get('end');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the distribution type of the current data according to [Galtung’s AJUS System]{@link https://en.wikipedia.org/wiki/Multimodal_distribution#Galtung.27s_classification}
|
||||
*
|
||||
* @return {string} Distribution type of current data
|
||||
* @api
|
||||
*/
|
||||
Histogram.prototype.getDistributionType = function () {
|
||||
if (this._internalModel) {
|
||||
var data = this._internalModel.getData();
|
||||
return this._internalModel.getDistributionType(data);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Histogram.prototype._validateBins = function (bins) {
|
||||
if (!_.isFinite(bins) || bins < 1 || Math.floor(bins) !== bins) {
|
||||
throw this._getValidationError('histogramInvalidBins');
|
||||
}
|
||||
};
|
||||
|
||||
Histogram.prototype._validateStartEnd = function (start, end) {
|
||||
const values = [start, end];
|
||||
|
||||
if (_.every(values, _.isUndefined)) return;
|
||||
|
||||
const bothAreNumbers = _.every(values, number => _.isNumber(number) && !_.isNaN(number));
|
||||
const bothAreNull = _.every(values, _.isNull);
|
||||
|
||||
if (!bothAreNumbers && !bothAreNull) {
|
||||
throw this._getValidationError('histogramInvalidStartEnd');
|
||||
}
|
||||
};
|
||||
|
||||
Histogram.prototype._checkOptions = function (options) {
|
||||
if (_.isUndefined(options)) {
|
||||
throw this._getValidationError('histogramOptionsRequired');
|
||||
}
|
||||
this._validateBins(options.bins);
|
||||
this._validateStartEnd(options.start, options.end);
|
||||
};
|
||||
|
||||
Histogram.prototype._createInternalModel = function (engine) {
|
||||
this._internalModel = new HistogramDataviewModel({
|
||||
source: this._source.$getInternalModel(),
|
||||
column: this._column,
|
||||
bins: this._bins,
|
||||
start: this._start,
|
||||
end: this._end,
|
||||
sync_on_bbox_change: !!this._boundingBoxFilter,
|
||||
sync_on_circle_change: !!this._circleFilter,
|
||||
sync_on_polygon_change: !!this._polygonFilter,
|
||||
enabled: this._enabled,
|
||||
column_type: 'number'
|
||||
}, {
|
||||
engine: engine,
|
||||
bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(),
|
||||
circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(),
|
||||
polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel()
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
/**
|
||||
* Fired when bins have changed. Handler gets a parameter with the new bins.
|
||||
*
|
||||
* @event binsChanged
|
||||
* @type {number}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,91 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Transform the data obtained from an internal histogram dataview into a
|
||||
* public object.
|
||||
*
|
||||
* @param {object[]} data - The raw histogram data
|
||||
* @param {number} nulls - Number of data with a null
|
||||
* @param {number} totalAmount - Total number of data in the histogram
|
||||
*
|
||||
* @return {carto.dataview.HistogramData} - The parsed and formatted data for the given parameters
|
||||
*/
|
||||
function parseHistogramData (data, nulls, totalAmount) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
var compactData = _.compact(data);
|
||||
var maxBin = _.max(compactData, function (bin) { return bin.freq || 0; });
|
||||
var maxFreq = _.isFinite(maxBin.freq) && maxBin.freq !== 0
|
||||
? maxBin.freq
|
||||
: null;
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Object containing histogram data.
|
||||
*
|
||||
* @typedef {object} carto.dataview.HistogramData
|
||||
* @property {number} nulls - The number of items with null value
|
||||
* @property {number} totalAmount - The number of elements returned
|
||||
* @property {carto.dataview.BinItem[]} bins - Array containing the {@link carto.dataview.BinItem|data bins} for the histogram
|
||||
* @property {string} type - String with value: **histogram**
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
bins: _createBins(compactData, maxFreq),
|
||||
nulls: nulls || 0,
|
||||
totalAmount: totalAmount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the histogram raw data into {@link carto.dataview.BinItem}
|
||||
*/
|
||||
function _createBins (data, maxFreq) {
|
||||
return data.map(function (bin) {
|
||||
/**
|
||||
* @example
|
||||
*
|
||||
* // We created an histogram containing airBnb prices per night
|
||||
* const histogramDataview = new carto.dataview.Histogram(airbnbDataset, 'price', { bins: 7 });
|
||||
* // Listen to dataChanged events
|
||||
* histogramDataview.on('dataChanged', data => {
|
||||
* // The first bin contains prices from 0 to 20€ per night, there are 3 rentals in this bin with a cost of 10 15 and 20€.
|
||||
* const bin = console.log(data.bins[0]);
|
||||
* // This is the bin index in the bins array
|
||||
* bin.index; // 0
|
||||
* // The first bin contains rentals from 0 to 20€ per night
|
||||
* bin.start; // 0
|
||||
* // The first bin contains rentals from 0 to 20€ per night
|
||||
* bin.end; // 20
|
||||
* // The lower rental in the bin is 10€ per night
|
||||
* bin.min; // 10
|
||||
* // The maximun rental in the bin is 20€ per night
|
||||
* bin.max; // 20
|
||||
* // The average price in this bin is 15€ per night
|
||||
* bin.avg; // 15
|
||||
* // The bin contains 3 prices
|
||||
* bin.freq; // 3
|
||||
* // Those 3 prices represent the 20% of the dataset.
|
||||
* bin.normalized; // 0.2
|
||||
* });
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @typedef {object} carto.dataview.BinItem
|
||||
* @property {number} index - Number indicating the bin order
|
||||
* @property {number} start - The lower limit of the bin
|
||||
* @property {number} end - The higher limit of the bin
|
||||
* @property {number} min - The minimal value appearing in the bin. Only appears if freq > 0
|
||||
* @property {number} max - The minimal value appearing in the bin. Only appears if freq > 0
|
||||
* @property {number} avg - The average value of the elements for this bin. Only appears if freq > 0
|
||||
* @property {number} freq - Number of elements in the bin
|
||||
* @property {number} normalized - Normalized frequency with respect to the whole data
|
||||
* @api
|
||||
*/
|
||||
return _.extend(bin, { normalized: _.isFinite(bin.freq) && maxFreq > 0 ? bin.freq / maxFreq : 0 });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = parseHistogramData;
|
||||
@@ -0,0 +1,19 @@
|
||||
var Category = require('./category');
|
||||
var Formula = require('./formula');
|
||||
var Histogram = require('./histogram');
|
||||
var TimeSeries = require('./time-series');
|
||||
var status = require('../constants').status;
|
||||
var timeAggregation = require('../constants').timeAggregation;
|
||||
|
||||
/**
|
||||
* @namespace carto.dataview
|
||||
* @api
|
||||
*/
|
||||
module.exports = {
|
||||
Category: Category,
|
||||
Formula: Formula,
|
||||
Histogram: Histogram,
|
||||
TimeSeries: TimeSeries,
|
||||
status: status,
|
||||
timeAggregation: timeAggregation
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('../base');
|
||||
var HistogramDataviewModel = require('../../../../dataviews/histogram-dataview-model');
|
||||
var parseTimeSeriesData = require('./parse-data');
|
||||
var timeAggregation = require('../../constants').timeAggregation;
|
||||
var isValidTimeAggregation = require('../../constants').isValidTimeAggregation;
|
||||
|
||||
/**
|
||||
* A dataview to represent an histogram of temporal data allowing to specify the granularity of the {@link carto.dataview.timeAggregation|temporal bins.}
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the dataview will fetch the data
|
||||
* @param {string} column - The column name to get the data
|
||||
* @param {object} [options]
|
||||
* @param {carto.dataview.timeAggregation} [options.aggregation=auto] - Granularity of time aggregation
|
||||
* @param {number} [options.offset] - Number of hours to offset the aggregation from UTC
|
||||
* @param {boolean} [options.useLocalTimezone] - Indicates whether to use the local user timezone, or not
|
||||
*
|
||||
* @fires dataChanged
|
||||
* @fires columnChanged
|
||||
* @fires statusChanged
|
||||
* @fires error
|
||||
*
|
||||
* @fires binsChanged
|
||||
* @fires aggregationChanged
|
||||
* @fires offsetChanged
|
||||
* @fires localTimezoneChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.dataview.Base
|
||||
* @memberof carto.dataview
|
||||
* @api
|
||||
* @example
|
||||
* // We have a tweets dataset and we want to show a "per hour histogram" with the data.
|
||||
* var timeSeries = new carto.dataview.TimeSeries(source0, 'last_review', {
|
||||
* offset: 0,
|
||||
* aggregation: 'hour'
|
||||
* });
|
||||
* @example
|
||||
* // You can listen to multiple events emmited by the time-series-dataview.
|
||||
* // Data and status are fired by all dataviews.
|
||||
* timeSeries.on('dataChanged', newData => { });
|
||||
* timeSeries.on('statusChanged', (newData, error) => { });
|
||||
* timeSeries.on('error', cartoError => { });
|
||||
*/
|
||||
function TimeSeries (source, column, options) {
|
||||
this._initialize(source, column, options);
|
||||
this._aggregation = this._options.aggregation;
|
||||
this._offset = _hoursToSeconds(this._options.offset);
|
||||
this._localTimezone = this._options.useLocalTimezone;
|
||||
}
|
||||
|
||||
TimeSeries.prototype = Object.create(Base.prototype);
|
||||
|
||||
TimeSeries.prototype.DEFAULTS = {
|
||||
aggregation: timeAggregation.AUTO,
|
||||
offset: 0,
|
||||
useLocalTimezone: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the resulting data.
|
||||
*
|
||||
* @return {carto.dataview.TimeSeriesData}
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.getData = function () {
|
||||
if (this._internalModel) {
|
||||
return parseTimeSeriesData(
|
||||
this._internalModel.get('data'),
|
||||
this._internalModel.get('nulls'),
|
||||
this._internalModel.get('totalAmount'),
|
||||
this._internalModel.getCurrentOffset()
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set time aggregation.
|
||||
*
|
||||
* @param {carto.dataview.timeAggregation} aggregation
|
||||
* @fires aggregationChanged
|
||||
* @return {carto.dataview.TimeSeries} this
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.setAggregation = function (aggregation) {
|
||||
this._validateAggregation(aggregation);
|
||||
this._changeProperty('aggregation', aggregation);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current time aggregation.
|
||||
*
|
||||
* @return {carto.dataview.timeAggregation} Current time aggregation
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.getAggregation = function () {
|
||||
return this._aggregation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set time offset in hours.
|
||||
*
|
||||
* @param {number} offset
|
||||
* @fires offsetChanged
|
||||
* @return {carto.dataview.TimeSeries} this
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.setOffset = function (offset) {
|
||||
this._validateOffset(offset);
|
||||
this._changeProperty('offset', _hoursToSeconds(offset));
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current time offset in hours.
|
||||
*
|
||||
* @return {number} Current time offset
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.getOffset = function () {
|
||||
return _secondsToHours(this._offset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the local timezone flag. If enabled, the time offset is overriden by the user's local timezone.
|
||||
*
|
||||
* @param {boolean} localTimezone
|
||||
* @fires localTimezoneChanged
|
||||
* @return {carto.dataview.TimeSeries} this
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.useLocalTimezone = function (enable) {
|
||||
this._validateLocalTimezone(enable);
|
||||
this._changeProperty('localTimezone', enable);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current local timezone flag.
|
||||
*
|
||||
* @return {boolean} Current local timezone flag
|
||||
* @api
|
||||
*/
|
||||
TimeSeries.prototype.isUsingLocalTimezone = function () {
|
||||
return this._localTimezone;
|
||||
};
|
||||
|
||||
TimeSeries.prototype._checkOptions = function (options) {
|
||||
if (_.isUndefined(options)) {
|
||||
throw this._getValidationError('timeSeriesOptionsRequired');
|
||||
}
|
||||
this._validateAggregation(options.aggregation);
|
||||
this._validateOffset(options.offset);
|
||||
this._validateLocalTimezone(options.useLocalTimezone);
|
||||
};
|
||||
|
||||
TimeSeries.prototype._validateAggregation = function (aggregation) {
|
||||
if (!isValidTimeAggregation(aggregation)) {
|
||||
throw this._getValidationError('timeSeriesInvalidAggregation');
|
||||
}
|
||||
};
|
||||
|
||||
TimeSeries.prototype._validateOffset = function (offset) {
|
||||
if (!_.isFinite(offset) || Math.floor(offset) !== offset || offset < -12 || offset > 14) {
|
||||
throw this._getValidationError('timeSeriesInvalidOffset');
|
||||
}
|
||||
};
|
||||
|
||||
TimeSeries.prototype._validateLocalTimezone = function (localTimezone) {
|
||||
if (!_.isBoolean(localTimezone)) {
|
||||
throw this._getValidationError('timeSeriesInvalidUselocaltimezone');
|
||||
}
|
||||
};
|
||||
|
||||
TimeSeries.prototype._createInternalModel = function (engine) {
|
||||
this._internalModel = new HistogramDataviewModel({
|
||||
source: this._source.$getInternalModel(),
|
||||
column: this._column,
|
||||
aggregation: this._aggregation,
|
||||
offset: this._offset,
|
||||
localTimezone: this._localTimezone,
|
||||
sync_on_bbox_change: !!this._boundingBoxFilter,
|
||||
sync_on_circle_change: !!this._circleFilter,
|
||||
sync_on_polygon_change: !!this._polygonFilter,
|
||||
enabled: this._enabled,
|
||||
column_type: 'date'
|
||||
}, {
|
||||
engine: engine,
|
||||
bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(),
|
||||
circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(),
|
||||
polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel()
|
||||
});
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
|
||||
function _hoursToSeconds (hours) {
|
||||
return hours * 3600;
|
||||
}
|
||||
|
||||
function _secondsToHours (seconds) {
|
||||
return seconds / 3600;
|
||||
}
|
||||
module.exports = TimeSeries;
|
||||
|
||||
/**
|
||||
* Fired when aggregation has changed. Handler gets a parameter with the new aggregation.
|
||||
*
|
||||
* @event aggregationChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when localTimezone has changed. Handler gets a parameter with the new timezone.
|
||||
*
|
||||
* @event localTimezoneChanged
|
||||
* @type {boolean}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when offset has changed. Handler gets a parameter with the new offset.
|
||||
*
|
||||
* @event offsetChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,73 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
function secondsToHours (seconds) {
|
||||
return seconds / 3600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the data obtained from an internal timeseries dataview into a public object.
|
||||
*
|
||||
* @param {object[]} data - The raw time series data
|
||||
* @param {number} nulls - Number of data with a null
|
||||
* @param {number} totalAmount - Total number of data in the histogram
|
||||
*
|
||||
* @return {TimeSeriesData} - The parsed and formatted data for the given parameters
|
||||
*/
|
||||
function parseTimeSeriesData (data, nulls, totalAmount, offset) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
var compactData = _.compact(data);
|
||||
var maxBin = _.max(compactData, function (bin) { return bin.freq || 0; });
|
||||
var maxFreq = _.isFinite(maxBin.freq) && maxBin.freq !== 0
|
||||
? maxBin.freq
|
||||
: null;
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Object containing time series data.
|
||||
*
|
||||
* @typedef {object} carto.dataview.TimeSeriesData
|
||||
* @property {number} nulls - The number of items with null value
|
||||
* @property {number} totalAmount - The number of elements returned
|
||||
* @property {number} offset - The time offset in hours. Needed to format UTC timestamps into the proper timezone format
|
||||
* @property {carto.dataview.TimeSeriesBinItem[]} bins - Array containing the {@link carto.dataview.TimeSeriesBinItem|data bins} for the time series
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
bins: _createBins(compactData, maxFreq),
|
||||
nulls: nulls || 0,
|
||||
offset: secondsToHours(offset),
|
||||
totalAmount: totalAmount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the time series raw data into {@link carto.dataview.TimeSeriesBinItem}.
|
||||
*/
|
||||
function _createBins (data, maxFreq) {
|
||||
return data.map(function (bin) {
|
||||
/**
|
||||
* @typedef {object} carto.dataview.TimeSeriesBinItem
|
||||
* @property {number} index - Number indicating the bin order
|
||||
* @property {number} start - Starting UTC timestamp of the bin
|
||||
* @property {number} end - End UTC timestamp of the bin
|
||||
* @property {number} min - Minimum UTC timestamp present in the bin. Only appears if freq > 0
|
||||
* @property {number} max - Maximum UTC timestamp present in the bin. Only appears if freq > 0
|
||||
* @property {number} freq - Numbers of elements present in the bin
|
||||
* @property {number} normalized - Normalized frequency with respect to the whole dataset
|
||||
* @api
|
||||
*/
|
||||
return {
|
||||
index: bin.bin,
|
||||
start: bin.start,
|
||||
end: bin.end,
|
||||
min: bin.min,
|
||||
max: bin.max,
|
||||
freq: bin.freq,
|
||||
normalized: _.isFinite(bin.freq) && maxFreq > 0 ? bin.freq / maxFreq : 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = parseTimeSeriesData;
|
||||
@@ -0,0 +1,92 @@
|
||||
var _ = require('underscore');
|
||||
var ERROR_LIST = require('./error-list');
|
||||
|
||||
/**
|
||||
* Returns two parameters to enrich a CartoError.
|
||||
* - friendlyMessage: A easy to understand error description.
|
||||
* - errorCode: Am unique error code
|
||||
*
|
||||
* @param {CartoError} cartoError
|
||||
*
|
||||
* @returns {object} - An object containing a friendly message and a errorCode
|
||||
*/
|
||||
function getExtraFields (cartoError) {
|
||||
var errorlist = _getErrorList(cartoError);
|
||||
var listedError = _getListedError(cartoError, errorlist);
|
||||
|
||||
return {
|
||||
friendlyMessage: listedError.friendlyMessage,
|
||||
errorCode: listedError.errorCode
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CartoError} cartoError
|
||||
*/
|
||||
function _getErrorList (cartoError) {
|
||||
return ERROR_LIST[cartoError.origin] && ERROR_LIST[cartoError.origin][cartoError.type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the listed error from a cartoError, if no listedError is found return a generic
|
||||
* unknown error.
|
||||
* @param {CartoError} cartoError
|
||||
*/
|
||||
function _getListedError (cartoError, errorList) {
|
||||
var errorListkeys = _.keys(errorList);
|
||||
var key;
|
||||
for (var i = 0; i < errorListkeys.length; i++) {
|
||||
key = errorListkeys[i];
|
||||
if (!(errorList[key].messageRegex instanceof RegExp)) {
|
||||
throw new Error('MessageRegex on ' + key + ' is not a RegExp.');
|
||||
}
|
||||
if (errorList[key].messageRegex.test(cartoError.message)) {
|
||||
return {
|
||||
friendlyMessage: _replaceRegex(cartoError, errorList[key]),
|
||||
errorCode: _buildErrorCode(cartoError, key)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// When cartoError not found return generic values
|
||||
return {
|
||||
friendlyMessage: cartoError.message || '',
|
||||
errorCode: _buildErrorCode(cartoError, 'unknown-error')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace $0 and $1 with the proper paramter in the listedError regex to build a friendly message
|
||||
*/
|
||||
function _replaceRegex (cartoError, listedError) {
|
||||
if (!listedError.friendlyMessage) {
|
||||
return cartoError.message;
|
||||
}
|
||||
var match = cartoError.message && cartoError.message.match(listedError.messageRegex);
|
||||
if (match && match.length > 1) {
|
||||
var replaced = listedError.friendlyMessage.replace('$0', match[1]);
|
||||
if (match.length > 2) {
|
||||
replaced = replaced.replace('$1', match[2]);
|
||||
}
|
||||
return replaced;
|
||||
}
|
||||
return listedError.friendlyMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an unique string that represents a cartoError
|
||||
* @param {cartoError} cartoError
|
||||
* @param {string} key
|
||||
*/
|
||||
function _buildErrorCode (cartoError, key) {
|
||||
var fragments = [];
|
||||
fragments.push(cartoError && cartoError.origin);
|
||||
fragments.push(cartoError && cartoError.type);
|
||||
fragments.push(key);
|
||||
fragments = _.compact(fragments);
|
||||
|
||||
return fragments.join(':');
|
||||
}
|
||||
|
||||
module.exports = { getExtraFields: getExtraFields };
|
||||
@@ -0,0 +1,137 @@
|
||||
var errorExtender = require('./carto-error-extender');
|
||||
var errorTracker = require('./error-tracker');
|
||||
|
||||
var UNEXPECTED_ERROR = 'unexpected error';
|
||||
var GENERIC_ORIGIN = 'generic';
|
||||
|
||||
/**
|
||||
* Build a cartoError from a generic error.
|
||||
* @constructor
|
||||
*
|
||||
* @return {CartoError} A well formed object representing the error.
|
||||
*/
|
||||
function CartoError (error, opts) {
|
||||
opts = opts || {};
|
||||
var cartoError = Object.create(Error.prototype);
|
||||
cartoError.message = (error && error.message) || UNEXPECTED_ERROR;
|
||||
cartoError.origin = (error && error.origin) || GENERIC_ORIGIN;
|
||||
cartoError.type = (error && error.type) || '';
|
||||
|
||||
if (_isWindshaftError(error)) {
|
||||
cartoError = _transformWindshaftError(error, opts.layers, opts.analysis);
|
||||
}
|
||||
|
||||
if (_isAjaxError(error)) {
|
||||
cartoError = _transformAjaxError(error);
|
||||
}
|
||||
|
||||
// Add extra fields
|
||||
var extraFields = errorExtender.getExtraFields(cartoError);
|
||||
cartoError.message = extraFields.friendlyMessage;
|
||||
cartoError.errorCode = extraFields.errorCode;
|
||||
|
||||
// Final properties
|
||||
cartoError.name = 'CartoError';
|
||||
cartoError.stack = (new Error()).stack;
|
||||
cartoError.originalError = error;
|
||||
|
||||
errorTracker.track(cartoError);
|
||||
|
||||
return cartoError;
|
||||
}
|
||||
|
||||
// Windshaft should have been parsed already
|
||||
function _isWindshaftError (error) {
|
||||
return error && error.origin === 'windshaft';
|
||||
}
|
||||
|
||||
function _isAjaxError (error) {
|
||||
return error && error.responseText;
|
||||
}
|
||||
|
||||
function _transformWindshaftError (error, layers, analysis) {
|
||||
var cartoError = Object.create(Error.prototype);
|
||||
cartoError.message = error.message;
|
||||
cartoError.origin = error.origin;
|
||||
cartoError.type = error.type;
|
||||
|
||||
if (error.type === 'layer' && layers) {
|
||||
cartoError.layer = layers.findById(error.layerId);
|
||||
}
|
||||
if (error.type === 'analysis') {
|
||||
if (analysis) {
|
||||
cartoError.source = analysis;
|
||||
cartoError.sourceId = analysis.getId && analysis.getId();
|
||||
}
|
||||
if (error.analysisId) {
|
||||
cartoError.sourceId = error.analysisId;
|
||||
}
|
||||
}
|
||||
|
||||
return cartoError;
|
||||
}
|
||||
|
||||
function _transformAjaxError (error) {
|
||||
var cartoError = Object.create(Error.prototype);
|
||||
cartoError.message = _handleAjaxResponse(error);
|
||||
cartoError.origin = 'ajax';
|
||||
cartoError.type = error.statusText;
|
||||
|
||||
return cartoError;
|
||||
}
|
||||
|
||||
function _handleAjaxResponse (error) {
|
||||
var errorMessage = '';
|
||||
|
||||
try {
|
||||
var parsedError = JSON.parse(error.responseText);
|
||||
errorMessage = parsedError.errors[0];
|
||||
} catch (exc) {
|
||||
// Swallow parse error
|
||||
}
|
||||
return errorMessage || UNEXPECTED_ERROR;
|
||||
}
|
||||
|
||||
module.exports = CartoError;
|
||||
|
||||
/**
|
||||
* Represents an error in the carto library.
|
||||
*
|
||||
* Some actions like adding a layer to a map are asynchronous and require a server round trip.
|
||||
* If some error happens during this communnication with the server, an error with a `CartoError` object
|
||||
* will be fired.
|
||||
*
|
||||
* CartoErrors can be obtained by listening to the client 'error' `client.on('error', callback);`,
|
||||
* through any async action or by listening to 'error' events on particular objects (eg: dataviews).
|
||||
*
|
||||
* Promises are also rejected with a CartoError.
|
||||
* @example
|
||||
* // Listen when a layer has been added or there has been an error.
|
||||
* client.addLayer(layerWithErrors)
|
||||
* .then(()=> console.log('Layer added succesfully'))
|
||||
* .catch(cartoError => console.error(cartoError.message))
|
||||
* @example
|
||||
* // Events also will be registered here when the map changes.
|
||||
* client.on('success', function () {
|
||||
* console.log('Client reloaded');
|
||||
* });
|
||||
*
|
||||
* client.on('error', function (clientError) {
|
||||
* console.error(clientError.message);
|
||||
* });
|
||||
* @example
|
||||
* // Listen when there is an error in a dataview
|
||||
* dataview.on('error', function (error) {
|
||||
* console.error(error.message);
|
||||
* });
|
||||
*
|
||||
* @typedef {object} CartoError
|
||||
* @property {string} message - A short error description
|
||||
* @property {string} name - The name of the error "CartoError"
|
||||
* @property {string} origin - Where the error was originated: 'windshaft' | 'ajax' | 'validation'
|
||||
* @property {object} originalError - An object containing the internal/original error
|
||||
* @property {object} stack - Error stack trace
|
||||
* @property {string} type - Error type
|
||||
* @property {string} sourceId - Available if the error is related to a source object. Indicates the ID of the source that has a problem.
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,17 @@
|
||||
var CartoError = require('./carto-error');
|
||||
|
||||
/**
|
||||
* Utility to build a cartoError related to validation errors.
|
||||
* @constructor
|
||||
*
|
||||
* @return {CartoError} A well formed object representing the error.
|
||||
*/
|
||||
function CartoValidationError (type, message, opts) {
|
||||
return new CartoError({
|
||||
origin: 'validation',
|
||||
type: type,
|
||||
message: message
|
||||
}, opts);
|
||||
}
|
||||
|
||||
module.exports = CartoValidationError;
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
var windshaft = require('./windshaft-errors');
|
||||
var ajax = require('./ajax-errors');
|
||||
var validation = require('./validation-errors');
|
||||
|
||||
module.exports = {
|
||||
ajax: ajax,
|
||||
windshaft: windshaft,
|
||||
validation: validation
|
||||
};
|
||||
@@ -0,0 +1,272 @@
|
||||
module.exports = {
|
||||
layer: {
|
||||
'non-valid-source': {
|
||||
messageRegex: /nonValidSource/,
|
||||
friendlyMessage: 'The given object is not a valid source. See "carto.source.Base".'
|
||||
},
|
||||
'non-valid-style': {
|
||||
messageRegex: /nonValidStyle/,
|
||||
friendlyMessage: 'The given object is not a valid style. See "carto.style.Base".'
|
||||
},
|
||||
'non-valid-columns': {
|
||||
messageRegex: /nonValidColumns/,
|
||||
friendlyMessage: 'The given object is not a valid array of string columns.'
|
||||
},
|
||||
'source-with-different-client': {
|
||||
messageRegex: /differentSourceClient/,
|
||||
friendlyMessage: "A layer can't have a source which belongs to a different client."
|
||||
},
|
||||
'style-with-different-client': {
|
||||
messageRegex: /differentStyleClient/,
|
||||
friendlyMessage: "A layer can't have a style which belongs to a different client."
|
||||
},
|
||||
'wrong-interactivity-columns': {
|
||||
messageRegex: /wrongInteractivityColumns\[(.+)\]#(.+)$/,
|
||||
friendlyMessage: 'Columns [$0] set on `$1` do not match the columns set in aggregation options.'
|
||||
}
|
||||
},
|
||||
source: {
|
||||
'query-required': {
|
||||
messageRegex: /requiredQuery/,
|
||||
friendlyMessage: 'SQL Source must have a SQL query.'
|
||||
},
|
||||
'query-string': {
|
||||
messageRegex: /requiredString/,
|
||||
friendlyMessage: 'SQL Query must be a string.'
|
||||
},
|
||||
'no-dataset-name': {
|
||||
messageRegex: /noDatasetName/,
|
||||
friendlyMessage: 'Table name is required.'
|
||||
},
|
||||
'dataset-string': {
|
||||
messageRegex: /requiredDatasetString$/,
|
||||
friendlyMessage: 'Table name must be a string.'
|
||||
},
|
||||
'dataset-required': {
|
||||
messageRegex: /requiredDataset$/,
|
||||
friendlyMessage: 'Table name must be not empty.'
|
||||
}
|
||||
},
|
||||
style: {
|
||||
'required-css': {
|
||||
messageRegex: /requiredCSS$/,
|
||||
friendlyMessage: 'CartoCSS is required.'
|
||||
},
|
||||
'css-string': {
|
||||
messageRegex: /requiredCSSString$/,
|
||||
friendlyMessage: 'CartoCSS must be a string.'
|
||||
}
|
||||
},
|
||||
client: {
|
||||
'bad-layer-type': {
|
||||
messageRegex: /badLayerType/,
|
||||
friendlyMessage: 'The given object is not a layer.'
|
||||
},
|
||||
'index-number': {
|
||||
messageRegex: /indexNumber/,
|
||||
friendlyMessage: 'index property must be a number.'
|
||||
},
|
||||
'index-out-of-range': {
|
||||
messageRegex: /indexOutOfRange/,
|
||||
friendlyMessage: 'index is out of range.'
|
||||
},
|
||||
'api-key-required': {
|
||||
messageRegex: /apiKeyRequired/,
|
||||
friendlyMessage: 'apiKey property is required.'
|
||||
},
|
||||
'api-key-string': {
|
||||
messageRegex: /apiKeyString/,
|
||||
friendlyMessage: 'apiKey property must be a string.'
|
||||
},
|
||||
'username-required': {
|
||||
messageRegex: /usernameRequired/,
|
||||
friendlyMessage: 'username property is required.'
|
||||
},
|
||||
'username-string': {
|
||||
messageRegex: /usernameString/,
|
||||
friendlyMessage: 'username property must be a string.'
|
||||
},
|
||||
'non-valid-server-url': {
|
||||
messageRegex: /nonValidServerURL/,
|
||||
friendlyMessage: 'serverUrl is not a valid URL.'
|
||||
},
|
||||
'non-matching-server-url': {
|
||||
messageRegex: /serverURLDoesntMatchUsername/,
|
||||
friendlyMessage: "serverUrl doesn't match the username."
|
||||
},
|
||||
'duplicated-layer-id': {
|
||||
messageRegex: /duplicatedLayerId/,
|
||||
friendlyMessage: 'A layer with the same ID already exists in the client.'
|
||||
}
|
||||
},
|
||||
dataview: {
|
||||
'source-required': {
|
||||
messageRegex: /sourceRequired/,
|
||||
friendlyMessage: 'Source property is required.'
|
||||
},
|
||||
'column-required': {
|
||||
messageRegex: /columnRequired/,
|
||||
friendlyMessage: 'Column property is required.'
|
||||
},
|
||||
'column-string': {
|
||||
messageRegex: /columnString/,
|
||||
friendlyMessage: 'Column property must be a string.'
|
||||
},
|
||||
'empty-column': {
|
||||
messageRegex: /emptyColumn/,
|
||||
friendlyMessage: 'Column property must be not empty.'
|
||||
},
|
||||
'filter-required': {
|
||||
messageRegex: /filterRequired/,
|
||||
friendlyMessage: 'Filter property is required.'
|
||||
},
|
||||
'time-series-options-required': {
|
||||
messageRegex: /timeSeriesOptionsRequired/,
|
||||
friendlyMessage: 'Options object to create a time series dataview is required.'
|
||||
},
|
||||
'time-series-invalid-aggregation': {
|
||||
messageRegex: /timeSeriesInvalidAggregation/,
|
||||
friendlyMessage: 'Time aggregation must be a valid value. Use carto.dataview.timeAggregation.'
|
||||
},
|
||||
'time-series-invalid-offset': {
|
||||
messageRegex: /timeSeriesInvalidOffset/,
|
||||
friendlyMessage: 'Offset must an integer value between -12 and 14.'
|
||||
},
|
||||
'time-series-invalid-uselocaltimezone': {
|
||||
messageRegex: /timeSeriesInvalidUselocaltimezone/,
|
||||
friendlyMessage: 'useLocalTimezone must be a boolean value.'
|
||||
},
|
||||
'histogram-options-required': {
|
||||
messageRegex: /histogramOptionsRequired/,
|
||||
friendlyMessage: 'Options object to create a histogram dataview is required.'
|
||||
},
|
||||
'histogram-invalid-bins': {
|
||||
messageRegex: /histogramInvalidBins/,
|
||||
friendlyMessage: 'Bins must be a positive integer value.'
|
||||
},
|
||||
'histogram-invalid-start-end': {
|
||||
messageRegex: /histogramInvalidStartEnd/,
|
||||
friendlyMessage: 'Both start and end values must be a number or null.'
|
||||
},
|
||||
'formula-options-required': {
|
||||
messageRegex: /formulaOptionsRequired/,
|
||||
friendlyMessage: 'Formula dataview options are not defined.'
|
||||
},
|
||||
'formula-invalid-operation': {
|
||||
messageRegex: /formulaInvalidOperation/,
|
||||
friendlyMessage: 'Operation for formula dataview is not valid. Use carto.operation'
|
||||
},
|
||||
'category-options-required': {
|
||||
messageRegex: /categoryOptionsRequired/,
|
||||
friendlyMessage: 'Category dataview options are not defined.'
|
||||
},
|
||||
'category-limit-required': {
|
||||
messageRegex: /categoryLimitRequired/,
|
||||
friendlyMessage: 'Limit for category dataview is required.'
|
||||
},
|
||||
'category-limit-number': {
|
||||
messageRegex: /categoryLimitNumber/,
|
||||
friendlyMessage: 'Limit for category dataview must be a number.'
|
||||
},
|
||||
'category-limit-positive': {
|
||||
messageRegex: /categoryLimitPositive/,
|
||||
friendlyMessage: 'Limit for category dataview must be greater than 0.'
|
||||
},
|
||||
'category-invalid-operation': {
|
||||
messageRegex: /categoryInvalidOperation/,
|
||||
friendlyMessage: 'Operation for category dataview is not valid. Use carto.operation'
|
||||
},
|
||||
'category-operation-required': {
|
||||
messageRegex: /categoryOperationRequired/,
|
||||
friendlyMessage: 'Operation column for category dataview is required.'
|
||||
},
|
||||
'category-operation-string': {
|
||||
messageRegex: /categoryOperationString/,
|
||||
friendlyMessage: 'Operation column for category dataview must be a string.'
|
||||
},
|
||||
'category-operation-empty': {
|
||||
messageRegex: /categoryOperationEmpty/,
|
||||
friendlyMessage: 'Operation column for category dataview must be not empty.'
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
'invalid-bounds-object': {
|
||||
messageRegex: /invalidBoundsObject/,
|
||||
friendlyMessage: 'Bounds object is not valid. Use a carto.filter.Bounds object'
|
||||
},
|
||||
'invalid-circle-object': {
|
||||
messageRegex: /invalidCircleObject/,
|
||||
friendlyMessage: 'Circle object is not valid. Use a carto.filter.CircleData object'
|
||||
},
|
||||
'invalid-polygon-object': {
|
||||
messageRegex: /invalidPolygonObject/,
|
||||
friendlyMessage: 'Polygon object is not valid. Use a carto.filter.PolygonData object'
|
||||
},
|
||||
'column-required': {
|
||||
messageRegex: /columnRequired/,
|
||||
friendlyMessage: 'Column property is required.'
|
||||
},
|
||||
'column-string': {
|
||||
messageRegex: /columnString/,
|
||||
friendlyMessage: 'Column property must be a string.'
|
||||
},
|
||||
'empty-column': {
|
||||
messageRegex: /emptyColumn/,
|
||||
friendlyMessage: 'Column property must be not empty.'
|
||||
},
|
||||
'invalid-filter': {
|
||||
messageRegex: /invalidFilter(.+)/,
|
||||
friendlyMessage: "'$0' is not a valid filter. Please check documentation."
|
||||
},
|
||||
'invalid-option': {
|
||||
messageRegex: /invalidOption(.+)/,
|
||||
friendlyMessage: "'$0' is not a valid option for this filter."
|
||||
},
|
||||
'wrong-filter-type': {
|
||||
messageRegex: /wrongFilterType/,
|
||||
friendlyMessage: 'Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.'
|
||||
},
|
||||
'invalid-parameter-type': {
|
||||
messageRegex: /invalidParameterType(.+)/,
|
||||
friendlyMessage: "Invalid parameter type for '$0'. Please check filters documentation."
|
||||
}
|
||||
},
|
||||
aggregation: {
|
||||
'threshold-required': {
|
||||
messageRegex: /thresholdRequired/,
|
||||
friendlyMessage: 'Aggregation threshold is required.'
|
||||
},
|
||||
'invalid-threshold': {
|
||||
messageRegex: /invalidThreshold/,
|
||||
friendlyMessage: 'Aggregation threshold must be an integer value greater than 0.'
|
||||
},
|
||||
'resolution-required': {
|
||||
messageRegex: /resolutionRequired/,
|
||||
friendlyMessage: 'Aggregation resolution is required.'
|
||||
},
|
||||
'invalid-resolution': {
|
||||
messageRegex: /invalidResolution/,
|
||||
friendlyMessage: 'Aggregation resolution must be 0.5, 1 or powers of 2 up to 256 (2, 4, 8, 16, 32, 64, 128, 256).'
|
||||
},
|
||||
'invalid-placement': {
|
||||
messageRegex: /invalidPlacement/,
|
||||
friendlyMessage: 'Aggregation placement is not valid. Must be one of these values: `point-sample`, `point-grid`, `centroid`'
|
||||
},
|
||||
'column-function-required': {
|
||||
messageRegex: /columnFunctionRequired(.+)$/,
|
||||
friendlyMessage: "Aggregation function for column '$0' is required."
|
||||
},
|
||||
'invalid-column-function': {
|
||||
messageRegex: /invalidColumnFunction(.+)$/,
|
||||
friendlyMessage: "Aggregation function for column '$0' is not valid. Use carto.aggregation.function"
|
||||
},
|
||||
'column-aggregated-column-required': {
|
||||
messageRegex: /columnAggregatedColumnRequired(.+)$/,
|
||||
friendlyMessage: "Column to be aggregated to '$0' is required."
|
||||
},
|
||||
'invalid-column-aggregated-column': {
|
||||
messageRegex: /invalidColumnAggregatedColumn(.+)$/,
|
||||
friendlyMessage: "Column to be aggregated to '$0' must be a string."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
module.exports = {
|
||||
analysis: {
|
||||
'sql-syntax-error': {
|
||||
messageRegex: /^syntax error/
|
||||
},
|
||||
'invalid-dataset': {
|
||||
messageRegex: /relation (.+) does not exist/,
|
||||
friendlyMessage: 'Invalid dataset name used. Dataset $0 does not exist.'
|
||||
},
|
||||
'column-does-not-exist': {
|
||||
messageRegex: /column (.+) does not exist/,
|
||||
friendlyMessage: 'Invalid column name. Column $0 does not exist.'
|
||||
},
|
||||
'analysis-requires-authentication': {
|
||||
messageRegex: /^Analysis requires authentication with API key/
|
||||
}
|
||||
},
|
||||
generic: {
|
||||
},
|
||||
limit: {
|
||||
'over-platform-limits': {
|
||||
messageRegex: /^You are over platform's limits/
|
||||
},
|
||||
'generic-limit-error': {
|
||||
messageRegex: /.*/,
|
||||
friendlyMessage: 'The server is taking too long to respond, due to poor conectivity or a temporary error with our servers. Please try again soon.'
|
||||
}
|
||||
},
|
||||
tile: {
|
||||
'generic-tile-error': {
|
||||
messageRegex: /.*/,
|
||||
friendlyMessage: 'Some tiles might not be rendering correctly.'
|
||||
}
|
||||
},
|
||||
layer: {
|
||||
'column-does-not-exist': {
|
||||
messageRegex: /column (.+) does not exist/,
|
||||
friendlyMessage: 'Invalid column name. Column $0 does not exist.'
|
||||
},
|
||||
'unrecognized-rule': {
|
||||
messageRegex: /Unrecognized rule: (.+)/,
|
||||
friendlyMessage: 'Unrecognized rule "$0"'
|
||||
},
|
||||
'generic-layer-error': {
|
||||
messageRegex: /.*/
|
||||
}
|
||||
},
|
||||
dataview: {
|
||||
'formula-does-not-support-operation': {
|
||||
messageRegex: /Formula does not support (.+) operation/
|
||||
},
|
||||
'column-does-not-exist': {
|
||||
messageRegex: /column (.+) does not exist/
|
||||
},
|
||||
'permission-denied': {
|
||||
messageRegex: /permission denied for (.+)/
|
||||
},
|
||||
'wrong-type-column-used-in-time-series': {
|
||||
messageRegex: /function date_part\(unknown, (.+)\) does not exist/,
|
||||
friendlyMessage: 'Your time series column type is $0. Please use a date type.'
|
||||
},
|
||||
'invalid-aggregation-value': {
|
||||
messageRegex: /Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year, decade, century, millennium/
|
||||
}
|
||||
},
|
||||
auth: {
|
||||
'forbidden': {
|
||||
messageRegex: /^Forbidden$/,
|
||||
friendlyMessage: 'Forbidden. API key does not grant access.'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Use {@link http://docs.trackjs.com/tracker/top-level-api} for error logging.
|
||||
*/
|
||||
function track (error) {
|
||||
if (window.trackJs) {
|
||||
try {
|
||||
var message = error ? error.message + ' - code: ' + error.errorCode : JSON.stringify(error);
|
||||
window.trackJs.track(new Error(message));
|
||||
} catch (exc) {
|
||||
// Swallow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {track: track};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Fired when something went wrong on the server side.
|
||||
*
|
||||
* @event error
|
||||
* @type {CartoError}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when a request to the server completed successfully.
|
||||
*
|
||||
* @event success
|
||||
* @api
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error'
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const FiltersCollection = require('./filters-collection');
|
||||
|
||||
/**
|
||||
* When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters.
|
||||
*
|
||||
* This filter will group as many filters as you want and it will add them to the query returning the rows that match ALL the filters to render the visualization.
|
||||
*
|
||||
* You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`.
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by room type, showing only private rooms
|
||||
* const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' });
|
||||
* // Create a filter by price, showing only listings lower than or equal to 50€
|
||||
* const priceFilter = new carto.filter.Range('price', { lte: 50 });
|
||||
*
|
||||
* // Combine the filters with an AND condition, returning rows that match both filters
|
||||
* const filterByRoomTypeAndPrice = new carto.filter.AND([ roomTypeFilter, priceFilter ]);
|
||||
*
|
||||
* // Add filters to the existing source
|
||||
* source.addFilter(filterByRoomTypeAndPrice);
|
||||
*
|
||||
* @class AND
|
||||
* @extends carto.filter.FiltersCollection
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
class AND extends FiltersCollection {
|
||||
constructor (filters) {
|
||||
super(filters);
|
||||
this.JOIN_OPERATOR = 'AND';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AND;
|
||||
@@ -0,0 +1,195 @@
|
||||
const _ = require('underscore');
|
||||
const Base = require('./base');
|
||||
const getObjectValue = require('../../../../src/util/get-object-value');
|
||||
|
||||
const ALLOWED_OPTIONS = ['includeNull'];
|
||||
const DEFAULT_JOIN_OPERATOR = 'AND';
|
||||
|
||||
/**
|
||||
* SQL Filter
|
||||
*
|
||||
* A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter
|
||||
*
|
||||
* @param {string} column - The filtering will be performed against this column
|
||||
* @param {object} [options={}]
|
||||
* @param {boolean} [options.includeNull] - Include null rows when returning data
|
||||
*
|
||||
* @class SQLBase
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
*/
|
||||
class SQLBase extends Base {
|
||||
constructor (column, options = {}) {
|
||||
super();
|
||||
|
||||
this._checkColumn(column);
|
||||
this._checkOptions(options);
|
||||
|
||||
this._column = column;
|
||||
this._filters = {};
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set any of the filter conditions, overwriting the previous one.
|
||||
* @param {string} filterType - The filter type that you want to set
|
||||
* @param {string} filterValue - The value of the filter
|
||||
*/
|
||||
set (filterType, filterValue) {
|
||||
if (!filterType || !filterValue || !_.isString(filterType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilter = { [filterType]: filterValue };
|
||||
|
||||
this._checkFilters(newFilter);
|
||||
this._filters[filterType] = filterValue;
|
||||
|
||||
this.trigger('change:filters', newFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter conditions, overriding all the previous ones.
|
||||
* @param {object} filters - The object containing all the new filters to apply.
|
||||
*/
|
||||
setFilters (filters) {
|
||||
if (!filters || !_.isObject(filters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkFilters(filters);
|
||||
this._filters = filters;
|
||||
|
||||
this.trigger('change:filters', filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all conditions from current filter
|
||||
*/
|
||||
resetFilters () {
|
||||
this.setFilters({});
|
||||
}
|
||||
|
||||
$getSQL () {
|
||||
const filters = Object.keys(this._filters);
|
||||
let sql = filters
|
||||
.map(filterType => this._interpolateFilter(filterType, this._filters[filterType]))
|
||||
.filter(filter => Boolean(filter))
|
||||
.join(` ${DEFAULT_JOIN_OPERATOR} `);
|
||||
|
||||
if (this._options.includeNull) {
|
||||
this._includeNullInQuery(sql);
|
||||
}
|
||||
|
||||
if (filters.length > 1) {
|
||||
return `(${sql})`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
_checkColumn (column) {
|
||||
if (_.isUndefined(column)) {
|
||||
throw this._getValidationError('columnRequired');
|
||||
}
|
||||
|
||||
if (!_.isString(column)) {
|
||||
throw this._getValidationError('columnString');
|
||||
}
|
||||
|
||||
if (_.isEmpty(column)) {
|
||||
throw this._getValidationError('emptyColumn');
|
||||
}
|
||||
}
|
||||
|
||||
_checkFilters (filters) {
|
||||
Object.keys(filters).forEach(filter => {
|
||||
const isFilterValid = _.contains(this.ALLOWED_FILTERS, filter);
|
||||
|
||||
if (!isFilterValid) {
|
||||
throw this._getValidationError(`invalidFilter${filter}`);
|
||||
}
|
||||
|
||||
const parameters = this.PARAMETER_SPECIFICATION[filter].parameters;
|
||||
const haveCorrectType = parameters.every(
|
||||
parameter => {
|
||||
const parameterValue = getObjectValue(filters, parameter.name);
|
||||
return parameter.allowedTypes.some(type => parameterIsOfType(type, parameterValue));
|
||||
}
|
||||
);
|
||||
|
||||
if (!haveCorrectType) {
|
||||
throw this._getValidationError(`invalidParameterType${filter}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_checkOptions (options) {
|
||||
Object.keys(options).forEach(option => {
|
||||
const isOptionValid = _.contains(ALLOWED_OPTIONS, option);
|
||||
|
||||
if (!isOptionValid) {
|
||||
throw this._getValidationError(`invalidOption${option}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_convertValueToSQLString (filterValue) {
|
||||
if (_.isDate(filterValue)) {
|
||||
return `'${filterValue.toISOString()}'`;
|
||||
}
|
||||
|
||||
if (_.isArray(filterValue)) {
|
||||
return filterValue
|
||||
.map(value => this._convertValueToSQLString(value))
|
||||
.join(',');
|
||||
}
|
||||
|
||||
if (_.isObject(filterValue)) {
|
||||
Object
|
||||
.keys(filterValue)
|
||||
.forEach(key => {
|
||||
if (key === 'query') {
|
||||
return;
|
||||
}
|
||||
|
||||
filterValue[key] = this._convertValueToSQLString(filterValue[key]);
|
||||
});
|
||||
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
if (_.isNumber(filterValue)) {
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
return `'${normalizeString(filterValue.toString())}'`;
|
||||
}
|
||||
|
||||
_interpolateFilter (filterType, filterValues) {
|
||||
const sqlString = _.template(this.SQL_TEMPLATES[filterType]);
|
||||
const value = this._convertValueToSQLString(filterValues);
|
||||
|
||||
return sqlString({ column: this._column, value });
|
||||
}
|
||||
|
||||
_includeNullInQuery (sql) {
|
||||
const filters = Object.keys(this._filters);
|
||||
|
||||
if (filters.length > 1) {
|
||||
sql = `(${sql})`;
|
||||
}
|
||||
|
||||
return `(${sql} OR ${this._column} IS NULL)`;
|
||||
}
|
||||
}
|
||||
|
||||
const parameterIsOfType = function (parameterType, parameterValue) {
|
||||
return _[`is${parameterType}`](parameterValue);
|
||||
};
|
||||
|
||||
const normalizeString = function (value) {
|
||||
return value.replace(/\n/g, '\\n').replace(/\"/g, '\\"').replace(/'/g, "''");
|
||||
};
|
||||
|
||||
module.exports = SQLBase;
|
||||
@@ -0,0 +1,45 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
|
||||
/**
|
||||
* Base filter object
|
||||
*
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
function Base () {}
|
||||
|
||||
_.extend(Base.prototype, Backbone.Events);
|
||||
|
||||
Base.prototype._getValidationError = function (code) {
|
||||
return new CartoValidationError('filter', code);
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
|
||||
/**
|
||||
* Fired when bounds have changed. Handler gets a parameter with the new bounds.
|
||||
*
|
||||
* @event boundsChanged
|
||||
* @type {carto.filter.Bounds}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when circle filter has changed. Handler gets a parameter with the new circle.
|
||||
*
|
||||
* @event circleChanged
|
||||
* @type {carto.filter.CircleData}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when polygon filter has changed. Handler gets a parameter with the new polygon.
|
||||
*
|
||||
* @event polygonChanged
|
||||
* @type {carto.filter.PolygonData}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,72 @@
|
||||
/* global google */
|
||||
|
||||
var Base = require('./base');
|
||||
var GoogleMapsBoundingBoxAdapter = require('../../../geo/adapters/gmaps-bounding-box-adapter');
|
||||
var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box');
|
||||
var utils = require('../../../core/util');
|
||||
var SpatialFilterTypes = require('./spatial-filter-types');
|
||||
|
||||
/**
|
||||
* Bounding box filter for Google Maps maps.
|
||||
*
|
||||
* When this filter is included into a dataview only the data inside the {@link https://developers.google.com/maps/documentation/javascript/3.exp/reference#Map|googleMap}
|
||||
* bounds will be taken into account.
|
||||
*
|
||||
* @param {google.maps.map} map - The google map to track the bounds
|
||||
*
|
||||
* @fires boundsChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*
|
||||
* @example
|
||||
* // Create a bonding box attached to a google map.
|
||||
* const bboxFilter = new carto.filter.BoundingBoxGoogleMaps(googleMap);
|
||||
* // Add the filter to a dataview. Generating new data when the map bounds are changed.
|
||||
* dataview.addFilter(bboxFilter);
|
||||
*/
|
||||
function BoundingBoxGoogleMaps (map) {
|
||||
if (!_isGoogleMap(map)) {
|
||||
throw new Error('Bounding box requires a Google Maps map but got: ' + map);
|
||||
}
|
||||
this.type = SpatialFilterTypes.BBOX;
|
||||
|
||||
// Adapt the Google Maps map to offer unique:
|
||||
// - getBounds() function
|
||||
// - 'boundsChanged' event
|
||||
var mapAdapter = new GoogleMapsBoundingBoxAdapter(map);
|
||||
// Use the adapter for the internal BoundingBoxFilter model
|
||||
this._internalModel = new BoundingBoxFilterModel(mapAdapter);
|
||||
this.listenTo(this._internalModel, 'boundsChanged', this._onBoundsChanged);
|
||||
}
|
||||
|
||||
BoundingBoxGoogleMaps.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Return the current bounds.
|
||||
*
|
||||
* @return {carto.filter.Bounds} Current bounds
|
||||
* @api
|
||||
*/
|
||||
BoundingBoxGoogleMaps.prototype.getBounds = function () {
|
||||
return this._internalModel.getBounds();
|
||||
};
|
||||
|
||||
BoundingBoxGoogleMaps.prototype._onBoundsChanged = function (bounds) {
|
||||
this.trigger('boundsChanged', bounds);
|
||||
};
|
||||
|
||||
BoundingBoxGoogleMaps.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
// Helper to check if an element is a leafletmap object
|
||||
function _isGoogleMap (element) {
|
||||
// Check if Google Maps is loaded
|
||||
utils.isGoogleMapsLoaded();
|
||||
return element instanceof google.maps.Map;
|
||||
}
|
||||
|
||||
module.exports = BoundingBoxGoogleMaps;
|
||||
@@ -0,0 +1,71 @@
|
||||
/* global L */
|
||||
var Base = require('./base');
|
||||
var LeafletBoundingBoxAdapter = require('../../../geo/adapters/leaflet-bounding-box-adapter');
|
||||
var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box');
|
||||
var utils = require('../../../core/util');
|
||||
var SpatialFilterTypes = require('./spatial-filter-types');
|
||||
|
||||
/**
|
||||
* Bounding box filter for Leaflet maps.
|
||||
*
|
||||
* When this filter is included into a dataview only the data inside the {@link http://leafletjs.com/reference-1.3.1.html#map|leafletMap}
|
||||
* bounds will be taken into account.
|
||||
*
|
||||
* @param {L.Map} map - The leaflet map view
|
||||
*
|
||||
* @fires boundsChanged
|
||||
*
|
||||
* @constructor
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*
|
||||
* @example
|
||||
* // Create a bonding box attached to a leaflet map.
|
||||
* const bboxFilter = new carto.filter.BoundingBoxLeaflet(leafletMap);
|
||||
* // Add the filter to a dataview. Generating new data when the map bounds are changed.
|
||||
* dataview.addFilter(bboxFilter);
|
||||
*/
|
||||
function BoundingBoxLeaflet (map) {
|
||||
if (!_isLeafletMap(map)) {
|
||||
throw new Error('Bounding box requires a Leaflet map but got: ' + map);
|
||||
}
|
||||
this.type = SpatialFilterTypes.BBOX;
|
||||
|
||||
// Adapt the Leaflet map to offer unique:
|
||||
// - getBounds() function
|
||||
// - 'boundsChanged' event
|
||||
var mapAdapter = new LeafletBoundingBoxAdapter(map);
|
||||
// Use the adapter for the internal BoundingBoxFilter model
|
||||
this._internalModel = new BoundingBoxFilterModel(mapAdapter);
|
||||
this.listenTo(this._internalModel, 'boundsChanged', this._onBoundsChanged);
|
||||
}
|
||||
|
||||
BoundingBoxLeaflet.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Return the current bounds.
|
||||
*
|
||||
* @return {carto.filter.Bounds} Current bounds
|
||||
* @api
|
||||
*/
|
||||
BoundingBoxLeaflet.prototype.getBounds = function () {
|
||||
return this._internalModel.getBounds();
|
||||
};
|
||||
|
||||
BoundingBoxLeaflet.prototype._onBoundsChanged = function (bounds) {
|
||||
this.trigger('boundsChanged', bounds);
|
||||
};
|
||||
|
||||
BoundingBoxLeaflet.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
// Helper to check if an element is a Leaflet map object
|
||||
function _isLeafletMap (element) {
|
||||
// Check if Leaflet is loaded
|
||||
utils.isLeafletLoaded();
|
||||
return element instanceof L.Map;
|
||||
}
|
||||
|
||||
module.exports = BoundingBoxLeaflet;
|
||||
@@ -0,0 +1,93 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var SpatialFilterTypes = require('./spatial-filter-types');
|
||||
|
||||
/**
|
||||
* Generic bounding box filter.
|
||||
*
|
||||
* When this filter is included into a dataview only the data inside a custom bounding box will be taken into account.
|
||||
*
|
||||
* You can manually set the bounds via the `.setBounds()` method.
|
||||
*
|
||||
* This filter could be useful if you want give the users to ability to select a portion of the map and update the dataviews accordingly.
|
||||
*
|
||||
*
|
||||
* @constructor
|
||||
* @fires boundsChanged
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*
|
||||
*/
|
||||
function BoundingBox () {
|
||||
this._internalModel = new BoundingBoxFilterModel();
|
||||
this.type = SpatialFilterTypes.BBOX;
|
||||
}
|
||||
|
||||
BoundingBox.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set the bounds.
|
||||
*
|
||||
* @param {carto.filter.Bounds} bounds
|
||||
* @fires boundsChanged
|
||||
* @return {carto.filter.BoundingBox} this
|
||||
* @api
|
||||
*/
|
||||
BoundingBox.prototype.setBounds = function (bounds) {
|
||||
this._checkBounds(bounds);
|
||||
this._internalModel.setBounds(bounds);
|
||||
this.trigger('boundsChanged', bounds);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the bounds.
|
||||
*
|
||||
* @fires boundsChanged
|
||||
* @return {carto.filter.BoundingBox} this
|
||||
* @api
|
||||
*/
|
||||
BoundingBox.prototype.resetBounds = function () {
|
||||
return this.setBounds({ west: 0, south: 0, east: 0, north: 0 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current bounds.
|
||||
*
|
||||
* @return {carto.filter.Bounds} Current bounds
|
||||
* @api
|
||||
*/
|
||||
BoundingBox.prototype.getBounds = function () {
|
||||
/**
|
||||
* @typedef {object} carto.filter.Bounds
|
||||
* @property {number} west - West coordinate
|
||||
* @property {number} south - South coordinate
|
||||
* @property {number} east - East coordinate
|
||||
* @property {number} north - North coordinate
|
||||
* @api
|
||||
*/
|
||||
return this._internalModel.getBounds();
|
||||
};
|
||||
|
||||
BoundingBox.prototype._checkBounds = function (bounds) {
|
||||
if (_.isUndefined(bounds) ||
|
||||
_.isUndefined(bounds.west) ||
|
||||
_.isUndefined(bounds.south) ||
|
||||
_.isUndefined(bounds.east) ||
|
||||
_.isUndefined(bounds.north) ||
|
||||
!_.isNumber(bounds.west) ||
|
||||
!_.isNumber(bounds.south) ||
|
||||
!_.isNumber(bounds.east) ||
|
||||
!_.isNumber(bounds.north)) {
|
||||
throw new CartoValidationError('filter', 'invalidBoundsObject');
|
||||
}
|
||||
};
|
||||
|
||||
BoundingBox.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
module.exports = BoundingBox;
|
||||
@@ -0,0 +1,107 @@
|
||||
const SQLBase = require('./base-sql');
|
||||
|
||||
const CATEGORY_COMPARISON_OPERATORS = {
|
||||
in: { parameters: [{ name: 'in', allowedTypes: ['Array', 'String', 'Object'] }] },
|
||||
notIn: { parameters: [{ name: 'notIn', allowedTypes: ['Array', 'String', 'Object'] }] },
|
||||
eq: { parameters: [{ name: 'eq', allowedTypes: ['String', 'Number', 'Date', 'Object'] }] },
|
||||
notEq: { parameters: [{ name: 'notEq', allowedTypes: ['String', 'Number', 'Date', 'Object'] }] },
|
||||
like: { parameters: [{ name: 'like', allowedTypes: ['String'] }] },
|
||||
similarTo: { parameters: [{ name: 'similarTo', allowedTypes: ['String'] }] }
|
||||
};
|
||||
|
||||
const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS));
|
||||
|
||||
/**
|
||||
* When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter.
|
||||
*
|
||||
* You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified.
|
||||
*
|
||||
* This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option.
|
||||
*
|
||||
* @param {string} column - The column which the filter will be performed against
|
||||
* @param {object} filters - The filters you want to apply to the table rows
|
||||
* @param {(string[]|object)} filters.in - Return rows whose column value is included within the provided values
|
||||
* @param {string} filters.in.query - Return rows whose column value is included within query results
|
||||
* @param {(string[]|object)} filters.notIn - Return rows whose column value is included within the provided values
|
||||
* @param {string} filters.notIn.query - Return rows whose column value is not included within query results
|
||||
* @param {(string|number|Date|object)} filters.eq - Return rows whose column value is equal to the provided value
|
||||
* @param {string} filters.eq.query - Return rows whose column value is equal to the value returned by query
|
||||
* @param {(string|number|Date|object)} filters.notEq - Return rows whose column value is not equal to the provided value
|
||||
* @param {string} filters.notEq.query - Return rows whose column value is not equal to the value returned by query
|
||||
* @param {string} filters.like - Return rows whose column value is like the provided value
|
||||
* @param {string} filters.similarTo - Return rows whose column value is similar to the provided values
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.includeNull] - Include null rows when returning data
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by room type, showing only private rooms
|
||||
* const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private Room' });
|
||||
* airbnbDataset.addFilter(roomTypeFilter);
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by room type, showing only private rooms and entire apartments
|
||||
* const roomTypeFilter = new carto.filter.Category('room_type', { in: ['Private Room', 'Entire home/apt'] });
|
||||
* airbnbDataset.addFilter(roomTypeFilter);
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by room type, showing results included in subquery
|
||||
* const roomTypeFilter = new carto.filter.Category('room_type', { in: { query: 'SELECT distinct(type) FROM rooms' } });
|
||||
* airbnbDataset.addFilter(roomTypeFilter);
|
||||
*
|
||||
* @class Category
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
class Category extends SQLBase {
|
||||
constructor (column, filters = {}, options) {
|
||||
super(column, options);
|
||||
|
||||
this.SQL_TEMPLATES = this._getSQLTemplates();
|
||||
this.ALLOWED_FILTERS = ALLOWED_FILTERS;
|
||||
this.PARAMETER_SPECIFICATION = CATEGORY_COMPARISON_OPERATORS;
|
||||
|
||||
this._checkFilters(filters);
|
||||
this._filters = filters;
|
||||
}
|
||||
|
||||
_getSQLTemplates () {
|
||||
return {
|
||||
in: '<% if (value) { %><%= column %> IN (<%= value.query || value %>)<% } else { %>true = false<% } %>',
|
||||
notIn: '<% if (value) { %><%= column %> NOT IN (<%= value.query || value %>)<% } %>',
|
||||
eq: '<%= column %> = <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
notEq: '<%= column %> != <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
like: '<%= column %> LIKE <%= value %>',
|
||||
similarTo: '<%= column %> SIMILAR TO <%= value %>'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set any of the filter conditions, overwriting the previous one.
|
||||
* @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`.
|
||||
* @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category}
|
||||
*
|
||||
* @memberof Category
|
||||
* @method set
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set filter conditions, overriding all the previous ones.
|
||||
* @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}.
|
||||
*
|
||||
* @memberof Category
|
||||
* @method setFilters
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remove all conditions from current filter
|
||||
*
|
||||
* @memberof Category
|
||||
* @method resetFilters
|
||||
* @api
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = Category;
|
||||
@@ -0,0 +1,90 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var CircleFilterModel = require('../../../windshaft/filters/circle');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var SpatialFilterTypes = require('./spatial-filter-types');
|
||||
|
||||
/**
|
||||
* Generic circle filter.
|
||||
*
|
||||
* When this filter is included into a dataview only the data inside a custom circle will be taken into account.
|
||||
*
|
||||
* You can manually set the circle properties with the `setCircle()`.
|
||||
*
|
||||
* This filter could be useful if you want give the users the ability to select a buffer around a point of interest in the map and update the dataviews accordingly.
|
||||
*
|
||||
*
|
||||
* @constructor
|
||||
* @fires circleChanged
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*
|
||||
*/
|
||||
function Circle () {
|
||||
this._internalModel = new CircleFilterModel();
|
||||
this.type = SpatialFilterTypes.CIRCLE;
|
||||
}
|
||||
|
||||
Circle.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set the circle.
|
||||
*
|
||||
* @param {carto.filter.CircleData} circle
|
||||
* @fires circleChanged
|
||||
* @return {carto.filter.Circle} this
|
||||
* @api
|
||||
*/
|
||||
Circle.prototype.setCircle = function (circle) {
|
||||
this._checkCircle(circle);
|
||||
this._internalModel.setCircle(circle);
|
||||
this.trigger('circleChanged', circle);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the circle.
|
||||
*
|
||||
* @fires circleChanged
|
||||
* @return {carto.filter.Circle} this
|
||||
* @api
|
||||
*/
|
||||
Circle.prototype.resetCircle = function () {
|
||||
return this.setCircle({ lat: 0, lng: 0, radius: 0 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current circle data
|
||||
*
|
||||
* @return {carto.filter.CircleData} Current circle data
|
||||
* @api
|
||||
*/
|
||||
Circle.prototype.getCircle = function () {
|
||||
/**
|
||||
* @typedef {object} carto.filter.CircleData
|
||||
* @property {number} lat - Center Latitude WGS84
|
||||
* @property {number} lng - Center Longitude WGS84
|
||||
* @property {number} radius - Radius in meters
|
||||
* @api
|
||||
*/
|
||||
return this._internalModel.getCircle();
|
||||
};
|
||||
|
||||
Circle.prototype._checkCircle = function (circle) {
|
||||
if (_.isUndefined(circle) ||
|
||||
_.isUndefined(circle.lat) ||
|
||||
_.isUndefined(circle.lng) ||
|
||||
_.isUndefined(circle.radius) ||
|
||||
!_.isNumber(circle.lat) ||
|
||||
!_.isNumber(circle.lng) ||
|
||||
!_.isNumber(circle.radius)) {
|
||||
throw new CartoValidationError('filter', 'invalidCircleObject');
|
||||
}
|
||||
};
|
||||
|
||||
Circle.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
module.exports = Circle;
|
||||
@@ -0,0 +1,112 @@
|
||||
const _ = require('underscore');
|
||||
const Base = require('./base');
|
||||
const SQLBase = require('./base-sql');
|
||||
|
||||
const DEFAULT_JOIN_OPERATOR = 'AND';
|
||||
|
||||
/**
|
||||
* Base class for AND and OR filters.
|
||||
*
|
||||
* Filters Collection is a way to group a set of filters in order to create composed filters, allowing the user to change the operator that joins the filters.
|
||||
*
|
||||
* **This object should not be used directly.**
|
||||
*
|
||||
* @class FiltersCollection
|
||||
* @abstract
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
class FiltersCollection extends Base {
|
||||
constructor (filters) {
|
||||
super();
|
||||
this._initialize(filters);
|
||||
}
|
||||
|
||||
_initialize (filters) {
|
||||
this._filters = [];
|
||||
|
||||
if (filters && filters.length) {
|
||||
filters.map(filter => this.addFilter(filter));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to collection
|
||||
*
|
||||
* @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter
|
||||
* @memberof FiltersCollection
|
||||
* @api
|
||||
*/
|
||||
addFilter (filter) {
|
||||
if (!(filter instanceof SQLBase) && !(filter instanceof FiltersCollection)) {
|
||||
throw this._getValidationError('wrongFilterType');
|
||||
}
|
||||
|
||||
if (_.contains(this._filters, filter)) return;
|
||||
|
||||
this.listenTo(filter, 'change:filters', filters => this._triggerFilterChange(filters));
|
||||
this._filters.push(filter);
|
||||
this._triggerFilterChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing filter from collection
|
||||
*
|
||||
* @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter
|
||||
* @returns {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} The removed element
|
||||
* @memberof FiltersCollection
|
||||
* @api
|
||||
*/
|
||||
removeFilter (filter) {
|
||||
const filterIndex = _.indexOf(this._filters, filter);
|
||||
if (filterIndex === -1) return;
|
||||
|
||||
const removedElement = this._filters.splice(filterIndex, 1)[0];
|
||||
removedElement.off('change:filters', null, this);
|
||||
|
||||
this._triggerFilterChange();
|
||||
return removedElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of added filters
|
||||
*
|
||||
* @returns {number} Number of contained filters
|
||||
* @memberof FiltersCollection
|
||||
* @api
|
||||
*/
|
||||
count () {
|
||||
return this._filters.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get added filters
|
||||
*
|
||||
* @returns {Array} Added filters
|
||||
* @memberof FiltersCollection
|
||||
* @api
|
||||
*/
|
||||
getFilters () {
|
||||
return this._filters;
|
||||
}
|
||||
|
||||
$getSQL () {
|
||||
const sqlFilters = this._filters.map(filter => filter.$getSQL())
|
||||
.filter(sqlString => Boolean(sqlString));
|
||||
|
||||
const joinedFilters = sqlFilters.join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `);
|
||||
|
||||
if (sqlFilters.length > 1) {
|
||||
return `(${joinedFilters})`;
|
||||
}
|
||||
|
||||
return joinedFilters;
|
||||
}
|
||||
|
||||
_triggerFilterChange (filters) {
|
||||
this.trigger('change:filters', filters);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FiltersCollection;
|
||||
@@ -0,0 +1,25 @@
|
||||
const BoundingBox = require('./bounding-box');
|
||||
const BoundingBoxLeaflet = require('./bounding-box-leaflet');
|
||||
const BoundingBoxGoogleMaps = require('./bounding-box-gmaps');
|
||||
const Circle = require('./circle');
|
||||
const Polygon = require('./polygon');
|
||||
const Category = require('./category');
|
||||
const Range = require('./range');
|
||||
const AND = require('./and');
|
||||
const OR = require('./or');
|
||||
|
||||
/**
|
||||
* @namespace carto.filter
|
||||
* @api
|
||||
*/
|
||||
module.exports = {
|
||||
BoundingBox,
|
||||
BoundingBoxLeaflet,
|
||||
BoundingBoxGoogleMaps,
|
||||
Circle,
|
||||
Polygon,
|
||||
Category,
|
||||
Range,
|
||||
AND,
|
||||
OR
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const FiltersCollection = require('./filters-collection');
|
||||
|
||||
/**
|
||||
* When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters.
|
||||
*
|
||||
* This filter will group as many filters as you want and it will add them to the query returning the rows that match ANY of the filters to render the visualization.
|
||||
*
|
||||
* You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`.
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by room type, showing only private rooms
|
||||
* const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' });
|
||||
* // Create a filter by price, showing only listings lower than or equal to 50€
|
||||
* const priceFilter = new carto.filter.Range('price', { lte: 50 });
|
||||
*
|
||||
* // Combine the filters with an OR operator, returning rows that match one or the other filter
|
||||
* const filterByRoomTypeOrPrice = new carto.filter.OR([ roomTypeFilter, priceFilter ]);
|
||||
*
|
||||
* // Add filters to the existing source
|
||||
* source.addFilter(filterByRoomTypeOrPrice);
|
||||
*
|
||||
* @class OR
|
||||
* @extends carto.filter.FiltersCollection
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
class OR extends FiltersCollection {
|
||||
/**
|
||||
* Create a OR group filter
|
||||
* @param {Array} filters - The filters to apply in the query
|
||||
*/
|
||||
constructor (filters) {
|
||||
super(filters);
|
||||
this.JOIN_OPERATOR = 'OR';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OR;
|
||||
@@ -0,0 +1,91 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var PolygonFilterModel = require('../../../windshaft/filters/polygon');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var SpatialFilterTypes = require('./spatial-filter-types');
|
||||
|
||||
/**
|
||||
* Generic polygon filter.
|
||||
*
|
||||
* When this filter is included into a dataview only the data inside a custom polygon will be taken into account.
|
||||
*
|
||||
* You can manually set the polygon with the `setPolygon()`.
|
||||
*
|
||||
* This filter could be useful if you want give the users the ability to select a custom area in the map and update the dataviews accordingly.
|
||||
*
|
||||
*
|
||||
* @constructor
|
||||
* @fires polygonChanged
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*
|
||||
*/
|
||||
function Polygon () {
|
||||
this._internalModel = new PolygonFilterModel();
|
||||
this.type = SpatialFilterTypes.POLYGON;
|
||||
}
|
||||
|
||||
Polygon.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set the polygon.
|
||||
*
|
||||
* @param {carto.filter.PolygonData} polygon
|
||||
* @fires polygonChanged
|
||||
* @return {carto.filter.Polygon} this
|
||||
* @api
|
||||
*/
|
||||
Polygon.prototype.setPolygon = function (polygon) {
|
||||
this._checkPolygon(polygon);
|
||||
this._internalModel.setPolygon(polygon);
|
||||
this.trigger('polygonChanged', polygon);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the polygon.
|
||||
*
|
||||
* @fires polygonChanged
|
||||
* @return {carto.filter.Polygon} this
|
||||
* @api
|
||||
*/
|
||||
Polygon.prototype.resetPolygon = function () {
|
||||
return this.setPolygon({
|
||||
type: 'Polygon',
|
||||
coordinates: []
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current polygon data
|
||||
*
|
||||
* @return {carto.filter.PolygonData} Current polygon data, expressed as a GeoJSON geometry fragment
|
||||
* @api
|
||||
*/
|
||||
Polygon.prototype.getPolygon = function () {
|
||||
/**
|
||||
* @typedef {object} carto.filter.PolygonData
|
||||
* @property {string} type - Geometry type, Just 'Polygon' is valid
|
||||
* @property {Array.<number[]>} coordinates - Array of coordinates [lng, lat] as defined in GeoJSON geometries
|
||||
* @api
|
||||
*/
|
||||
return this._internalModel.getPolygon();
|
||||
};
|
||||
|
||||
Polygon.prototype._checkPolygon = function (polygon) {
|
||||
if (_.isUndefined(polygon) ||
|
||||
_.isUndefined(polygon.type) ||
|
||||
_.isUndefined(polygon.coordinates) ||
|
||||
!_.isString(polygon.type) ||
|
||||
!_.isArray(polygon.coordinates) ||
|
||||
polygon.type !== 'Polygon') {
|
||||
throw new CartoValidationError('filter', 'invalidPolygonObject');
|
||||
}
|
||||
};
|
||||
|
||||
Polygon.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
module.exports = Polygon;
|
||||
@@ -0,0 +1,140 @@
|
||||
const SQLBase = require('./base-sql');
|
||||
|
||||
const RANGE_COMPARISON_OPERATORS = {
|
||||
lt: { parameters: [{ name: 'lt', allowedTypes: ['Number', 'Date', 'Object'] }] },
|
||||
lte: { parameters: [{ name: 'lte', allowedTypes: ['Number', 'Date', 'Object'] }] },
|
||||
gt: { parameters: [{ name: 'gt', allowedTypes: ['Number', 'Date', 'Object'] }] },
|
||||
gte: { parameters: [{ name: 'gte', allowedTypes: ['Number', 'Date', 'Object'] }] },
|
||||
between: {
|
||||
parameters: [
|
||||
{ name: 'between.min', allowedTypes: ['Number', 'Date'] },
|
||||
{ name: 'between.max', allowedTypes: ['Number', 'Date'] }
|
||||
]
|
||||
},
|
||||
notBetween: {
|
||||
parameters: [
|
||||
{ name: 'notBetween.min', allowedTypes: ['Number', 'Date'] },
|
||||
{ name: 'notBetween.max', allowedTypes: ['Number', 'Date'] }
|
||||
]
|
||||
},
|
||||
betweenSymmetric: {
|
||||
parameters: [
|
||||
{ name: 'betweenSymmetric.min', allowedTypes: ['Number', 'Date'] },
|
||||
{ name: 'betweenSymmetric.max', allowedTypes: ['Number', 'Date'] }
|
||||
]
|
||||
},
|
||||
notBetweenSymmetric: {
|
||||
parameters: [
|
||||
{ name: 'notBetweenSymmetric.min', allowedTypes: ['Number', 'Date'] },
|
||||
{ name: 'notBetweenSymmetric.max', allowedTypes: ['Number', 'Date'] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS));
|
||||
|
||||
/**
|
||||
* When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter.
|
||||
*
|
||||
* You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified.
|
||||
*
|
||||
* This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option.
|
||||
*
|
||||
* @param {string} column - The column to filter rows
|
||||
* @param {object} filters - The filters you want to apply to the column
|
||||
* @param {(number|Date|object)} filters.lt - Return rows whose column value is less than the provided value
|
||||
* @param {string} filters.lt.query - Return rows whose column value is less than the value returned by query
|
||||
* @param {(number|Date|object)} filters.lte - Return rows whose column value is less than or equal to the provided value
|
||||
* @param {string} filters.lte.query - Return rows whose column value is less than or equal to the value returned by query
|
||||
* @param {(number|Date|object)} filters.gt - Return rows whose column value is greater than the provided value
|
||||
* @param {string} filters.gt.query - Return rows whose column value is greater than the value returned by query
|
||||
* @param {(number|Date|object)} filters.gte - Return rows whose column value is greater than or equal to the provided value
|
||||
* @param {string} filters.gte.query - Return rows whose column value is greater than or equal to the value returned by query
|
||||
* @param {(number|Date)} filters.between - Return rows whose column value is between the provided values
|
||||
* @param {(number|Date)} filters.between.min - Lower value of the comparison range
|
||||
* @param {(number|Date)} filters.between.max - Upper value of the comparison range
|
||||
* @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values
|
||||
* @param {(number|Date)} filters.notBetween.min - Lower value of the comparison range
|
||||
* @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range
|
||||
* @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them
|
||||
* @param {(number|Date)} filters.betweenSymmetric.min - Lower value of the comparison range
|
||||
* @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range
|
||||
* @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them
|
||||
* @param {(number|Date)} filters.notBetweenSymmetric.min - Lower value of the comparison range
|
||||
* @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.includeNull] - Include null rows when returning data
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by price, showing only listings lower than or equal to 50€, and higher than 100€
|
||||
* const priceFilter = new carto.filter.Range('price', { lte: 50, gt: 100 });
|
||||
*
|
||||
* // Add filter to the existing source
|
||||
* airbnbDataset.addFilter(priceFilter);
|
||||
*
|
||||
* @example
|
||||
* // Create a filter by price, showing only listings greater than or equal to the average price
|
||||
* const priceFilter = new carto.filter.Range('price', { gte: { query: 'SELECT avg(price) FROM listings' } });
|
||||
*
|
||||
* // Add filter to the existing source
|
||||
* airbnbDataset.addFilter(priceFilter);
|
||||
*
|
||||
* @class Range
|
||||
* @extends carto.filter.Base
|
||||
* @memberof carto.filter
|
||||
* @api
|
||||
*/
|
||||
class Range extends SQLBase {
|
||||
constructor (column, filters = {}, options) {
|
||||
super(column, options);
|
||||
|
||||
this.SQL_TEMPLATES = this._getSQLTemplates();
|
||||
this.ALLOWED_FILTERS = ALLOWED_FILTERS;
|
||||
this.PARAMETER_SPECIFICATION = RANGE_COMPARISON_OPERATORS;
|
||||
|
||||
this._checkFilters(filters);
|
||||
this._filters = filters;
|
||||
}
|
||||
|
||||
_getSQLTemplates () {
|
||||
return {
|
||||
lt: '<%= column %> < <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
lte: '<%= column %> <= <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
gt: '<%= column %> > <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
gte: '<%= column %> >= <%= value.query ? "(" + value.query + ")" : value %>',
|
||||
between: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>',
|
||||
notBetween: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>',
|
||||
betweenSymmetric: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>',
|
||||
notBetweenSymmetric: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set any of the filter conditions, overwriting the previous one.
|
||||
* @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`.
|
||||
* @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range}
|
||||
*
|
||||
* @memberof Range
|
||||
* @method set
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set filter conditions, overriding all the previous ones.
|
||||
* @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Range}.
|
||||
*
|
||||
* @memberof Range
|
||||
* @method setFilters
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remove all conditions from current filter
|
||||
*
|
||||
* @memberof Range
|
||||
* @method resetFilters
|
||||
* @api
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = Range;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Types of spatial filters
|
||||
*/
|
||||
var types = {
|
||||
BBOX: 'bbox',
|
||||
CIRCLE: 'circle',
|
||||
POLYGON: 'polygon'
|
||||
};
|
||||
|
||||
module.exports = types;
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @api
|
||||
* @namespace carto
|
||||
*
|
||||
* @description
|
||||
* # CARTO.js
|
||||
* All the library features are exposed through the `carto` namespace.
|
||||
*
|
||||
*
|
||||
* - **Client** : The api client.
|
||||
* - **source** : Source description
|
||||
* - **style** : Style description
|
||||
* - **layer** : Layer description
|
||||
* - **dataview** : Dataview description
|
||||
* - **filter** : Filter description
|
||||
* - **events** : The events exposed.
|
||||
* - **operation** : The operations exposed.
|
||||
*/
|
||||
|
||||
// Add polyfill for `fetch`
|
||||
require('whatwg-fetch');
|
||||
// Add polyfill for `Promise`
|
||||
var Promise = require('promise-polyfill');
|
||||
if (!window.Promise) {
|
||||
window.Promise = Promise;
|
||||
}
|
||||
|
||||
var Client = require('./client');
|
||||
var source = require('./source');
|
||||
var style = require('./style');
|
||||
var layer = require('./layer');
|
||||
var dataview = require('./dataview');
|
||||
var filter = require('./filter');
|
||||
var events = require('./events');
|
||||
var constants = require('./constants');
|
||||
|
||||
var carto = {
|
||||
version: require('../../../package.json').version,
|
||||
ATTRIBUTION: constants.ATTRIBUTION,
|
||||
Client: Client,
|
||||
source: source,
|
||||
style: style,
|
||||
layer: layer,
|
||||
dataview: dataview,
|
||||
filter: filter,
|
||||
events: events,
|
||||
operation: constants.operation
|
||||
};
|
||||
|
||||
module.exports = carto;
|
||||
@@ -0,0 +1,178 @@
|
||||
var _ = require('underscore');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
|
||||
/**
|
||||
* List of possible aggregation operations.
|
||||
* See {@link https://carto.com/developers/maps-api/tile-aggregation#columns } for more info.
|
||||
* @enum {string} carto.layer.Aggregation.operation
|
||||
* @memberof carto.layer.Aggregation
|
||||
* @api
|
||||
*/
|
||||
var OPERATIONS = {
|
||||
/** The new point will contain the average value of the or the aggregated ones */
|
||||
AVG: 'avg',
|
||||
/** The new point will contain the sum of the aggregated values */
|
||||
SUM: 'sum',
|
||||
/** The new point will contain the minimal value existing the aggregated features */
|
||||
MIN: 'min',
|
||||
/** The new point will contain the maximun value existing the aggregated features */
|
||||
MAX: 'max',
|
||||
/** The new point will contain the mode of the aggregated values */
|
||||
MODE: 'mode'
|
||||
};
|
||||
|
||||
/**
|
||||
* List of possible aggregation feature placements.
|
||||
* See {@link https://carto.com/developers/maps-api/tile-aggregation#placement } for more info.
|
||||
* @enum {string} carto.layer.Aggregation.placement
|
||||
* @memberof carto.layer.Aggregation
|
||||
* @api
|
||||
*/
|
||||
var PLACEMENTS = {
|
||||
/** The new point will be placed at a random sample of the aggregated points */
|
||||
SAMPLE: 'point-sample',
|
||||
/** The new point will be placed at the center of the aggregation grid cells */
|
||||
GRID: 'point-grid',
|
||||
/** The new point will be placed at averaged coordinated of the grouped points */
|
||||
CENTROID: 'centroid'
|
||||
};
|
||||
|
||||
var VALID_RESOLUTIONS = [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256];
|
||||
|
||||
/**
|
||||
* An aggregation can be passed to a {@link carto.layer.Layer} to reduce the number of visible points
|
||||
* increasing the performance.
|
||||
*
|
||||
* See {@link https://carto.com/developers/maps-api/guides/tile-aggregation/} for more info.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {number} opts.threshold - The minimum number of rows in the dataset for aggregation to be applied
|
||||
* @param {number} opts.resolution - The cell-size of the spatial aggregation grid [more info]{@link https://carto.com/developers/maps-api/tile-aggregation#resolution}
|
||||
* @param {string} opts.placement - The kind of [aggregated geometry]{@link https://carto.com/developers/maps-api/tile-aggregation#placement} generated
|
||||
* @param {object} opts.columns - The new columns are computed by a applying an aggregate function to all the points in each group
|
||||
* @param {string} opts.columns.aggregatedFunction - The Function used to aggregate the points: avg (average), sum, min (minimum), max (maximum) and mode (the most frequent value in the group)
|
||||
* @param {string} opts.columns.aggregatedColumn - The name of the original column to be aggregated.
|
||||
*
|
||||
* @example
|
||||
* // Create a layer with aggregated data.
|
||||
* const aggregationOptions = {
|
||||
* // CARTO applies aggregation if your dataset has more than threshold rows. In this case, more than 1 row.
|
||||
* threshold: 1,
|
||||
* // Defines the cell-size of the aggregation grid. In this case, 1x1 pixel.
|
||||
* resolution: 1,
|
||||
* // Where the new point will be placed. In this case, at the center of the grid.
|
||||
* placement: carto.layer.Aggregation.placement.GRID,
|
||||
* // Here we define the aggregated columns that we want to obtain.
|
||||
* columns: {
|
||||
* // Each property key is the name of the new generated column
|
||||
* avg_population: {
|
||||
* // The aggregated column will contain the average of the original data.
|
||||
* aggregateFunction: carto.layer.Aggregation.operation.AVG,
|
||||
* // The column to be aggregated
|
||||
* aggregatedColumn: 'population'
|
||||
* }, {
|
||||
* min_population: {
|
||||
* aggregateFunction: carto.layer.Aggregation.operation.MIN,
|
||||
* aggregatedColumn: 'population'
|
||||
* }
|
||||
* };
|
||||
* const aggregation = new Aggregation(options);
|
||||
* const layer = new carto.layer.Layer(source, style, { aggregation: aggregation });
|
||||
*
|
||||
* @constructor
|
||||
* @api
|
||||
* @memberof carto.layer
|
||||
*/
|
||||
function Aggregation (opts) {
|
||||
if (!_.isFinite(opts.threshold)) {
|
||||
throw _getValidationError('thresholdRequired');
|
||||
}
|
||||
|
||||
if (!_.isFinite(opts.threshold) || opts.threshold < 1 || Math.floor(opts.threshold) !== opts.threshold) {
|
||||
throw _getValidationError('invalidThreshold');
|
||||
}
|
||||
|
||||
if (!_.isFinite(opts.resolution)) {
|
||||
throw _getValidationError('resolutionRequired');
|
||||
}
|
||||
|
||||
if (!_.contains(VALID_RESOLUTIONS, opts.resolution)) {
|
||||
throw _getValidationError('invalidResolution');
|
||||
}
|
||||
|
||||
_checkValidPlacement(opts.placement);
|
||||
|
||||
var columns = _checkAndTransformColumns(opts.columns);
|
||||
|
||||
var aggregation = {
|
||||
threshold: opts.threshold,
|
||||
resolution: opts.resolution,
|
||||
placement: opts.placement,
|
||||
columns: columns
|
||||
};
|
||||
|
||||
return _.pick(aggregation, _.identity); // Remove empty values
|
||||
}
|
||||
|
||||
Aggregation.operation = OPERATIONS;
|
||||
|
||||
Aggregation.placement = PLACEMENTS;
|
||||
|
||||
function _checkColumns (columns) {
|
||||
Object.keys(columns).forEach(function (key) {
|
||||
_checkColumn(columns, key);
|
||||
});
|
||||
}
|
||||
|
||||
function _checkColumn (columns, key) {
|
||||
if (!columns[key].aggregatedColumn) {
|
||||
throw _getValidationError('columnAggregatedColumnRequired' + key);
|
||||
}
|
||||
|
||||
if (!_.isString(columns[key].aggregatedColumn)) {
|
||||
throw _getValidationError('invalidColumnAggregatedColumn' + key);
|
||||
}
|
||||
|
||||
if (!columns[key].aggregateFunction) {
|
||||
throw _getValidationError('columnFunctionRequired' + key);
|
||||
}
|
||||
|
||||
if (!_.contains(_.values(OPERATIONS), columns[key].aggregateFunction)) {
|
||||
throw _getValidationError('invalidColumnFunction' + key);
|
||||
}
|
||||
}
|
||||
|
||||
function _getValidationError (code) {
|
||||
return new CartoValidationError('aggregation', code);
|
||||
}
|
||||
|
||||
// Windshaft uses snake_case for column parameters
|
||||
function _checkAndTransformColumns (columns) {
|
||||
var returnValue = null;
|
||||
|
||||
if (columns) {
|
||||
_checkColumns(columns);
|
||||
|
||||
returnValue = {};
|
||||
Object.keys(columns).forEach(function (key) {
|
||||
returnValue[key] = _columnToSnakeCase(columns[key]);
|
||||
});
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
// Windshaft uses snake_case for column parameters
|
||||
function _columnToSnakeCase (column) {
|
||||
return {
|
||||
aggregate_function: column.aggregateFunction,
|
||||
aggregated_column: column.aggregatedColumn
|
||||
};
|
||||
}
|
||||
|
||||
function _checkValidPlacement (placement) {
|
||||
if (placement && !_.contains(_.values(PLACEMENTS), placement)) {
|
||||
throw _getValidationError('invalidPlacement');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Aggregation;
|
||||
@@ -0,0 +1,51 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Base layer object.
|
||||
*
|
||||
* This object should not be used directly! use {@link carto.layer.Layer} instead.
|
||||
*
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @fires error
|
||||
* @memberof carto.layer
|
||||
* @api
|
||||
*/
|
||||
function Base (source, layer, options) {
|
||||
options = options || {};
|
||||
this._id = options.id || Base.$generateId();
|
||||
}
|
||||
|
||||
_.extend(Base.prototype, Backbone.Events);
|
||||
|
||||
/**
|
||||
* Get the unique autogenerated id.
|
||||
*
|
||||
* @return {string} Unique autogenerated id
|
||||
*
|
||||
*/
|
||||
Base.prototype.getId = function () {
|
||||
return this._id;
|
||||
};
|
||||
|
||||
/**
|
||||
* The instance id will be autogenerated by incrementing this variable.
|
||||
*/
|
||||
Base.$nextId = 0;
|
||||
|
||||
/**
|
||||
* Static funciton used internally to autogenerate source ids.
|
||||
*/
|
||||
Base.$generateId = function () {
|
||||
return 'L' + ++Base.$nextId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the real CARTO.js model used by the layer.
|
||||
*/
|
||||
Base.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Events fired by a layer
|
||||
*
|
||||
* @enum {string}
|
||||
* @readonly
|
||||
* @memberof carto.layer
|
||||
*/
|
||||
var events = {
|
||||
/**
|
||||
* A feature has been clicked, fired every time the user clicks on a feature.
|
||||
*/
|
||||
FEATURE_CLICKED: 'featureClicked',
|
||||
/**
|
||||
* The mouse is over a feature, fired every time the user moves over a feature.
|
||||
*/
|
||||
FEATURE_OVER: 'featureOver',
|
||||
/**
|
||||
* The mouse exits a feature, fired every time the user moves out of a feature.
|
||||
*/
|
||||
FEATURE_OUT: 'featureOut',
|
||||
/**
|
||||
* There has been an error related to tiles, fired every time the features are not rendered due to an error.
|
||||
*/
|
||||
TILE_ERROR: 'featureError'
|
||||
};
|
||||
|
||||
module.exports = events;
|
||||
@@ -0,0 +1,18 @@
|
||||
var Layer = require('./layer');
|
||||
var EventTypes = require('./event-types');
|
||||
var Aggregation = require('./aggregation');
|
||||
|
||||
/**
|
||||
* @namespace carto.layer
|
||||
* @api
|
||||
*/
|
||||
module.exports = {
|
||||
Aggregation: Aggregation,
|
||||
Layer: Layer,
|
||||
events: EventTypes
|
||||
};
|
||||
|
||||
/**
|
||||
* @namespace carto.layer.metadata
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,576 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var CartoDBLayer = require('../../../geo/map/cartodb-layer');
|
||||
var SourceBase = require('../source/base');
|
||||
var StyleBase = require('../style/base');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var EVENTS = require('../events');
|
||||
var metadataParser = require('./metadata/parser');
|
||||
|
||||
/**
|
||||
* Represents a layer Object.
|
||||
*
|
||||
* A layer is the primary way to visualize geospatial data.
|
||||
*
|
||||
* To create a layer a {@link carto.source.Base|source} and {@link carto.style.Base|styles}
|
||||
* are required:
|
||||
*
|
||||
* - The {@link carto.source.Base|source} is used to know **what** data will be displayed in the Layer.
|
||||
* - The {@link carto.style.Base|style} is used to know **how** to draw the data in the Layer.
|
||||
*
|
||||
* A layer alone won't do too much. In order to get data from the CARTO server you must add the Layer to a {@link carto.Client|client}.
|
||||
*
|
||||
* ```
|
||||
* // Create a layer. Remember this won't do anything unless the layer is added to a client.
|
||||
* const layer = new carto.layer.Layer(source, style);
|
||||
*```
|
||||
*
|
||||
* @param {carto.source.Base} source - The source where the layer will fetch the data
|
||||
* @param {carto.style.CartoCSS} style - A CartoCSS object with the layer styling
|
||||
* @param {object} [options]
|
||||
* @param {Array<string>} [options.featureClickColumns=[]] - Columns that will be available for `featureClick` events
|
||||
* @param {boolean} [options.visible=true] - A boolean value indicating the layer's visibility
|
||||
* @param {Array<string>} [options.featureOverColumns=[]] - Columns that will be available for `featureOver` events
|
||||
* @param {carto.layer.Aggregation} [options.aggregation={}] - Specify {@link carto.layer.Aggregation|aggregation } options
|
||||
* @param {string} [options.id] - An unique identifier for the layer
|
||||
* @fires metadataChanged
|
||||
* @fires featureClicked
|
||||
* @fires featureOut
|
||||
* @fires featureOver
|
||||
* @fires error
|
||||
* @example
|
||||
* const citiesSource = new carto.source.SQL('SELECT * FROM cities');
|
||||
* const citiesStyle = new carto.style.CartoCSS(`
|
||||
* #layer {
|
||||
* marker-fill: #FABADA;
|
||||
* marker-width: 10;
|
||||
* }
|
||||
* `);
|
||||
* // Create a layer with no options
|
||||
* new carto.layer.Layer(citiesSource, citiesStyle);
|
||||
* @example
|
||||
* const citiesSource = new carto.source.SQL('SELECT * FROM cities');
|
||||
* const citiesStyle = new carto.style.CartoCSS(`
|
||||
* #layer {
|
||||
* marker-fill: #FABADA;
|
||||
* marker-width: 10;
|
||||
* }
|
||||
* `);
|
||||
* // Create a layer indicating what columns will be included in the featureOver event.
|
||||
* new carto.layer.Layer(citiesSource, citiesStyle, {
|
||||
* featureOverColumns: [ 'name' ]
|
||||
* });
|
||||
* @example
|
||||
* const citiesSource = new carto.source.SQL('SELECT * FROM cities');
|
||||
* const citiesStyle = new carto.style.CartoCSS(`
|
||||
* #layer {
|
||||
* marker-fill: #FABADA;
|
||||
* marker-width: 10;
|
||||
* }
|
||||
* `);
|
||||
* // Create a hidden layer
|
||||
* new carto.layer.Layer(citiesSource, citiesStyle, { visible: false });
|
||||
* @example
|
||||
* // Listen to the event thrown when the mouse is over a feature
|
||||
* layer.on('featureOver', featureEvent => {
|
||||
* console.log(`Mouse over city with name: ${featureEvent.data.name}`);
|
||||
* });
|
||||
* @constructor
|
||||
* @extends carto.layer.Base
|
||||
* @memberof carto.layer
|
||||
* @api
|
||||
*/
|
||||
function Layer (source, style, options = {}) {
|
||||
Base.apply(this, arguments);
|
||||
|
||||
_checkSource(source);
|
||||
_checkStyle(style);
|
||||
|
||||
this._client = undefined;
|
||||
this._engine = undefined;
|
||||
this._internalModel = undefined;
|
||||
|
||||
this._source = source;
|
||||
this._style = style;
|
||||
this._visible = _.isBoolean(options.visible) ? options.visible : true;
|
||||
this._featureClickColumns = options.featureClickColumns || [];
|
||||
this._featureOverColumns = options.featureOverColumns || [];
|
||||
this._minzoom = options.minzoom || 0;
|
||||
this._maxzoom = options.maxzoom || undefined;
|
||||
this._aggregation = options.aggregation || {};
|
||||
|
||||
_validateAggregationColumnsAndInteractivity(this._aggregation.columns, this._featureClickColumns, this._featureOverColumns);
|
||||
}
|
||||
|
||||
Layer.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Set a new style for this layer.
|
||||
*
|
||||
* @param {carto.style.CartoCSS} style - New style
|
||||
* @fires styleChanged
|
||||
* @fires error
|
||||
* @return {Promise} A promise that will be fulfilled when the style is applied to the layer or rejected with a
|
||||
* {@link CartoError} if something goes bad
|
||||
*/
|
||||
Layer.prototype.setStyle = function (style, opts) {
|
||||
var prevStyle = this._style;
|
||||
_checkStyle(style);
|
||||
opts = opts || {};
|
||||
if (prevStyle === style) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!this._internalModel) {
|
||||
this._style = style;
|
||||
this.trigger('styleChanged', this);
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If style has an engine and is different from the layer`s engine throw an error
|
||||
if (style.$getEngine() && style.$getEngine() !== this._internalModel._engine) {
|
||||
throw new CartoValidationError('layer', 'differentStyleClient');
|
||||
}
|
||||
// If style has no engine, set the layer engine in the style.
|
||||
if (!style.$getEngine()) {
|
||||
style.$setEngine(this._engine);
|
||||
}
|
||||
|
||||
this._internalModel.set('cartocss', style.getContent(), { silent: true });
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
this._style = style;
|
||||
this.trigger('styleChanged', this);
|
||||
}.bind(this))
|
||||
.catch(_rejectAndTriggerError.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current style for this layer.
|
||||
*
|
||||
* @return {carto.style.CartoCSS} Current style
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.getStyle = function () {
|
||||
return this._style;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a new source for this layer.
|
||||
*
|
||||
* A source and a layer must belong to the same client so you can't
|
||||
* add a source belonging to a different client.
|
||||
*
|
||||
* @param {carto.source.Base} source - New source
|
||||
* @fires sourceChanged
|
||||
* @fires error
|
||||
* @return {Promise} A promise that will be fulfilled when the style is applied to the layer or rejected with a
|
||||
* {@link CartoError} if something goes bad
|
||||
*/
|
||||
Layer.prototype.setSource = function (source) {
|
||||
var prevSource = this._source;
|
||||
_checkSource(source);
|
||||
if (prevSource === source) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If layer is not instantiated just store the new status
|
||||
if (!this._internalModel) {
|
||||
this._source = source;
|
||||
this.trigger('sourceChanged', this);
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If layer has been instantiated
|
||||
// If the source already has an engine and is different from the layer's engine throw an error.
|
||||
if (source.$getEngine() && source.$getEngine() !== this._internalModel._engine) {
|
||||
throw new CartoValidationError('layer', 'differentSourceClient');
|
||||
}
|
||||
// If source has no engine use the layer engine.
|
||||
if (!source.$getEngine()) {
|
||||
source.$setEngine(this._engine);
|
||||
}
|
||||
// Update the internalModel and return a promise
|
||||
this._internalModel.set('source', source.$getInternalModel(), { silent: true });
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
this._source = source;
|
||||
this.trigger('sourceChanged', this);
|
||||
}.bind(this))
|
||||
.catch(_rejectAndTriggerError.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current source for this layer.
|
||||
*
|
||||
* @return {carto.source.Base} Current source
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.getSource = function () {
|
||||
return this._source;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set new columns for featureClick events.
|
||||
*
|
||||
* @param {Array<string>} columns - An array containing column names
|
||||
* @fires error
|
||||
* @return {Promise}
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.setFeatureClickColumns = function (columns) {
|
||||
var prevColumns = this._featureClickColumns;
|
||||
_checkColumns(columns);
|
||||
if (_areColumnsTheSame(columns, prevColumns)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If layer is not instantiated just store the new status
|
||||
if (!this._internalModel) {
|
||||
this._featureClickColumns = columns;
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Update the internalModel and return a promise
|
||||
this._internalModel.infowindow.fields.set(_getInteractivityFields(columns).fields, { silent: true });
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
this._featureClickColumns = columns;
|
||||
}.bind(this))
|
||||
.catch(_rejectAndTriggerError.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the columns available in featureClicked events.
|
||||
*
|
||||
* @return {Array<string>} Column names available in featureClicked events
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.getFeatureClickColumns = function () {
|
||||
return this._featureClickColumns;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set new columns for featureOver events.
|
||||
*
|
||||
* @param {Array<string>} columns - An array containing column names
|
||||
* @fires error
|
||||
* @return {Promise}
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.setFeatureOverColumns = function (columns) {
|
||||
var prevColumns = this._featureOverColumns;
|
||||
_checkColumns(columns);
|
||||
if (_areColumnsTheSame(columns, prevColumns)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If layer is not instantiated just store the new status
|
||||
if (!this._internalModel) {
|
||||
this._featureOverColumns = columns;
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Update the internalModel and return a promise
|
||||
this._internalModel.tooltip.fields.set(_getInteractivityFields(columns).fields, { silent: true });
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
this._featureOverColumns = columns;
|
||||
}.bind(this))
|
||||
.catch(_rejectAndTriggerError.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the columns available in featureOver events.
|
||||
*
|
||||
* @return {Array<string>} Column names available in featureOver events
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.getFeatureOverColumns = function () {
|
||||
return this._featureOverColumns;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the layer.
|
||||
*
|
||||
* @fires visibilityChanged
|
||||
* @return {carto.layer.Layer} this
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.hide = function () {
|
||||
var prevStatus = this._visible;
|
||||
this._visible = false;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set('visible', false);
|
||||
}
|
||||
if (prevStatus) {
|
||||
this.trigger('visibilityChanged', false);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the layer.
|
||||
*
|
||||
* @fires visibilityChanged
|
||||
* @return {carto.layer.Layer} this
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.show = function () {
|
||||
var prevStatus = this._visible;
|
||||
this._visible = true;
|
||||
if (this._internalModel) {
|
||||
this._internalModel.set('visible', true);
|
||||
}
|
||||
if (!prevStatus) {
|
||||
this.trigger('visibilityChanged', false);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the layer's visibility.
|
||||
*
|
||||
* @fires visibilityChanged
|
||||
* @return {carto.layer.Layer} this
|
||||
*/
|
||||
Layer.prototype.toggle = function () {
|
||||
return this.isVisible() ? this.hide() : this.show();
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the layer is visible and false when not visible.
|
||||
*
|
||||
* @return {boolean} - A boolean value indicating the layer's visibility
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.isVisible = function () {
|
||||
return this._visible;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return `true` if the layer is not visible and `false` when visible.
|
||||
*
|
||||
* @return {boolean} - A boolean value indicating the layer's visibility
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.isHidden = function () {
|
||||
return !this.isVisible();
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the layer has interactivity.
|
||||
*
|
||||
* @return {boolean} - A boolean value indicating the layer's interactivity
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.isInteractive = function () {
|
||||
return this.getFeatureClickColumns().length > 0 || this.getFeatureOverColumns().length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the layer's order.
|
||||
*
|
||||
* @param {number} index - new order index for the layer.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.setOrder = function (index) {
|
||||
if (!this._client) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._client.moveLayer(this, index);
|
||||
};
|
||||
|
||||
/**
|
||||
* Move the layer to the back.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.bringToBack = function () {
|
||||
return this.setOrder(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Move the layer to the front.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @api
|
||||
*/
|
||||
Layer.prototype.bringToFront = function () {
|
||||
return this.setOrder(this._client._layers.size() - 1);
|
||||
};
|
||||
|
||||
// Private functions.
|
||||
|
||||
Layer.prototype._createInternalModel = function (engine) {
|
||||
var internalModel = new CartoDBLayer({
|
||||
id: this._id,
|
||||
source: this._source.$getInternalModel(),
|
||||
cartocss: this._style.getContent(),
|
||||
visible: this._visible,
|
||||
infowindow: _getInteractivityFields(this._featureClickColumns),
|
||||
tooltip: _getInteractivityFields(this._featureOverColumns),
|
||||
minzoom: this._minzoom,
|
||||
maxzoom: this._maxzoom
|
||||
}, {
|
||||
engine: engine,
|
||||
aggregation: this._aggregation
|
||||
});
|
||||
|
||||
internalModel.on('change:meta', function (layer, data) {
|
||||
var rules = data.cartocss_meta.rules;
|
||||
var styleMetadataList = metadataParser.getMetadataFromRules(rules);
|
||||
|
||||
/**
|
||||
* Event fired by {@link carto.layer.Layer} when the style contains any TurboCarto ramp.
|
||||
*
|
||||
* @typedef {object} carto.layer.MetadataEvent
|
||||
* @property {carto.layer.metadata.Base[]} styles - List of style metadata objects
|
||||
* @api
|
||||
*/
|
||||
var metadata = { styles: styleMetadataList };
|
||||
|
||||
this.trigger('metadataChanged', metadata);
|
||||
}, this);
|
||||
|
||||
internalModel.on('change:error', function (model, value) {
|
||||
if (value && _isStyleError(value)) {
|
||||
this._style.$setError(new CartoError(value));
|
||||
} else if (value) {
|
||||
this.trigger(EVENTS.ERROR, new CartoError(value));
|
||||
}
|
||||
}, this);
|
||||
|
||||
return internalModel;
|
||||
};
|
||||
|
||||
// Internal functions.
|
||||
|
||||
Layer.prototype.$setClient = function (client) {
|
||||
// Exit if the client is already set or
|
||||
// it has a different engine than the layer
|
||||
if (this._client || (this._engine && client._engine !== this._engine)) {
|
||||
return;
|
||||
}
|
||||
this._client = client;
|
||||
};
|
||||
|
||||
Layer.prototype.$setEngine = function (engine) {
|
||||
if (this._engine) {
|
||||
return;
|
||||
}
|
||||
this._engine = engine;
|
||||
this._source.$setEngine(engine);
|
||||
this._style.$setEngine(engine);
|
||||
if (!this._internalModel) {
|
||||
this._internalModel = this._createInternalModel(engine);
|
||||
this._style.on('$changed', function (style) {
|
||||
this._internalModel.set('cartocss', style.getContent(), { silent: true });
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
// Scope functions
|
||||
|
||||
/**
|
||||
* Transform the columns array into the format expected by the CartoDBLayer.
|
||||
*/
|
||||
function _getInteractivityFields (columns) {
|
||||
var fields = columns.map(function (column, index) {
|
||||
return {
|
||||
name: column,
|
||||
title: true,
|
||||
position: index
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
fields: fields
|
||||
};
|
||||
}
|
||||
|
||||
function _checkStyle (style) {
|
||||
if (!(style instanceof StyleBase)) {
|
||||
throw new CartoValidationError('layer', 'nonValidStyle');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkSource (source) {
|
||||
if (!(source instanceof SourceBase)) {
|
||||
throw new CartoValidationError('layer', 'nonValidSource');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkColumns (columns) {
|
||||
if (_.any(columns, function (item) { return !_.isString(item); })) {
|
||||
throw new CartoValidationError('layer', 'nonValidColumns');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when a windshaft error is because a styling error.
|
||||
*/
|
||||
function _isStyleError (windshaftError) {
|
||||
return windshaftError.message && windshaftError.message.indexOf('style') >= 0;
|
||||
}
|
||||
|
||||
function _rejectAndTriggerError (err) {
|
||||
var error = new CartoError(err);
|
||||
this.trigger(EVENTS.ERROR, error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
function _areColumnsTheSame (newColumns, oldColumns) {
|
||||
return newColumns.length === oldColumns.length && _.isEmpty(_.difference(newColumns, oldColumns));
|
||||
}
|
||||
|
||||
/**
|
||||
* When there are aggregated columns and interactivity columns they must agree
|
||||
*/
|
||||
function _validateAggregationColumnsAndInteractivity (aggregationColumns, clickColumns, overColumns) {
|
||||
var aggColumns = (aggregationColumns && Object.keys(aggregationColumns)) || [];
|
||||
|
||||
_validateColumnsConcordance(aggColumns, clickColumns, 'featureClick');
|
||||
_validateColumnsConcordance(aggColumns, overColumns, 'featureOver');
|
||||
}
|
||||
|
||||
function _validateColumnsConcordance (aggColumns, interactivityColumns, interactivity) {
|
||||
if (interactivityColumns.length > 0 && aggColumns.length > 0) {
|
||||
var notInAggregation = _.filter(interactivityColumns, function (clickColumn) {
|
||||
return !_.contains(aggColumns, clickColumn);
|
||||
});
|
||||
|
||||
if (notInAggregation.length > 0) {
|
||||
throw new CartoValidationError('layer', 'wrongInteractivityColumns[' + notInAggregation.join(', ') + ']#' + interactivity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} LatLng
|
||||
* @property {number} lat - Latitude
|
||||
* @property {number} lng - Longitude
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when the source has changed. Handler gets a parameter with the new source.
|
||||
*
|
||||
* @event sourceChanged
|
||||
* @type {carto.layer.Layer}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when the style has changed. Handler gets a parameter with the new style.
|
||||
*
|
||||
* @event styleChanged
|
||||
* @type {carto.layer.Layer}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when style metadata has changed.
|
||||
*
|
||||
* @event metadataChanged
|
||||
* @type {carto.layer.MetadataEvent}
|
||||
* @api
|
||||
*/
|
||||
|
||||
module.exports = Layer;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Base metadata object
|
||||
*
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @memberof carto.layer.metadata
|
||||
* @api
|
||||
*/
|
||||
function Base (type, rule) {
|
||||
this._type = type || '';
|
||||
this._column = rule.getColumn();
|
||||
this._mapping = rule.getMapping();
|
||||
this._property = rule.getProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the metadata
|
||||
*
|
||||
* @return {string}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getType = function () {
|
||||
return this._type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the column of the metadata
|
||||
*
|
||||
* @return {string}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getColumn = function () {
|
||||
return this._column;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the property of the metadata
|
||||
*
|
||||
* @return {string}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getMapping = function () {
|
||||
return this._mapping;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the property of the metadata
|
||||
*
|
||||
* @return {string}
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getProperty = function () {
|
||||
return this._property;
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
@@ -0,0 +1,95 @@
|
||||
var Base = require('./base');
|
||||
|
||||
/**
|
||||
* Metadata type buckets
|
||||
*
|
||||
* Adding a Turbocarto ramp (with ranges) in the style generates a response
|
||||
* from the server with the resulting information, after computing the ramp.
|
||||
* This information is wrapped in a metadata object of type 'buckets', that
|
||||
* contains a list of buckets with the range (min, max) and the value. And
|
||||
* also the total min, max range and the average of the total values.
|
||||
*
|
||||
* For example, the following ramp will generate a metadata of type 'buckets'
|
||||
* with numeric values (the size) in its buckets:
|
||||
*
|
||||
* marker-width: ramp([scalerank], range(5, 20), quantiles(5));
|
||||
*
|
||||
* In another example, this ramp will generate a metadata of type 'buckets'
|
||||
* with string values (the color) in its buckets:
|
||||
*
|
||||
* marker-fill: ramp([scalerank], (#FFC6C4, #EE919B, #CC607D), quantiles);
|
||||
*
|
||||
* @param {object} rule - Rule with the cartocss metadata
|
||||
* @constructor
|
||||
* @hideconstructor
|
||||
* @extends carto.layer.metadata.Base
|
||||
* @memberof carto.layer.metadata
|
||||
* @api
|
||||
*/
|
||||
function Buckets (rule) {
|
||||
var rangeBuckets = rule.getBucketsWithRangeFilter();
|
||||
|
||||
/**
|
||||
* @typedef {object} carto.layer.metadata.Bucket
|
||||
* @property {number} min - The minimum range value
|
||||
* @property {number} max - The maximum range value
|
||||
* @property {number|string} value - The value of the bucket
|
||||
* @api
|
||||
*/
|
||||
this._buckets = rangeBuckets.map(function (bucket) {
|
||||
return {
|
||||
min: bucket.filter.start,
|
||||
max: bucket.filter.end,
|
||||
value: bucket.value
|
||||
};
|
||||
});
|
||||
this._avg = rule.getFilterAvg();
|
||||
this._min = rangeBuckets.length > 0 ? rangeBuckets[0].filter.start : undefined;
|
||||
this._max = rangeBuckets.length > 0 ? rangeBuckets[rangeBuckets.length - 1].filter.end : undefined;
|
||||
|
||||
Base.call(this, 'buckets', rule);
|
||||
}
|
||||
|
||||
Buckets.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Return the buckets
|
||||
*
|
||||
* @return {carto.layer.metadata.Bucket[]}
|
||||
* @api
|
||||
*/
|
||||
Buckets.prototype.getBuckets = function () {
|
||||
return this._buckets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the average of the column
|
||||
*
|
||||
* @return {number}
|
||||
* @api
|
||||
*/
|
||||
Buckets.prototype.getAverage = function () {
|
||||
return this._avg;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the minimum value in the ranges
|
||||
*
|
||||
* @return {number}
|
||||
* @api
|
||||
*/
|
||||
Buckets.prototype.getMin = function () {
|
||||
return this._min;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the maximum value in the ranges
|
||||
*
|
||||
* @return {number}
|
||||
* @api
|
||||
*/
|
||||
Buckets.prototype.getMax = function () {
|
||||
return this._max;
|
||||
};
|
||||
|
||||
module.exports = Buckets;
|
||||
@@ -0,0 +1,68 @@
|
||||
var Base = require('./base');
|
||||
|
||||
/**
|
||||
* Metadata type categories
|
||||
*
|
||||
* Adding a Turbocarto ramp (with categories) in the style generates a response
|
||||
* from the server with the resulting information after computing the ramp.
|
||||
* This information is wrapped in a metadata object of type 'categories', that
|
||||
* contains a list of categories with the name of the category and the value. And
|
||||
* also the default value if it has been defined in the ramp.
|
||||
*
|
||||
* For example, the following ramp will generate a metadata of type 'categories'
|
||||
* with string values (the color) in its categories. The #CCCCCC is the default
|
||||
* value in this case:
|
||||
*
|
||||
* marker-fill: ramp([scalerank], (#F54690, #D16996, #CCCCCC), (1, 2), "=", category);
|
||||
*
|
||||
* @param {object} rule - Rule with the cartocss metadata
|
||||
* @constructor
|
||||
* @hideconstructor
|
||||
* @extends carto.layer.metadata.Base
|
||||
* @memberof carto.layer.metadata
|
||||
* @api
|
||||
*/
|
||||
function Categories (rule) {
|
||||
var categoryBuckets = rule.getBucketsWithCategoryFilter();
|
||||
var defaultBuckets = rule.getBucketsWithDefaultFilter();
|
||||
|
||||
/**
|
||||
* @typedef {object} carto.layer.metadata.Category
|
||||
* @property {number|string} name - The name of the category
|
||||
* @property {string} value - The value of the category
|
||||
* @api
|
||||
*/
|
||||
this._categories = categoryBuckets.map(function (bucket) {
|
||||
return {
|
||||
name: bucket.filter.name,
|
||||
value: bucket.value
|
||||
};
|
||||
});
|
||||
this._defaultValue = defaultBuckets.length > 0 ? defaultBuckets[0].value : undefined;
|
||||
|
||||
Base.call(this, 'categories', rule);
|
||||
}
|
||||
|
||||
Categories.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Return the buckets
|
||||
*
|
||||
* @return {carto.layer.metadata.Category[]}
|
||||
* @api
|
||||
*/
|
||||
Categories.prototype.getCategories = function () {
|
||||
return this._categories;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the default value
|
||||
*
|
||||
* @return {string}
|
||||
* @api
|
||||
*/
|
||||
Categories.prototype.getDefaultValue = function () {
|
||||
return this._defaultValue;
|
||||
};
|
||||
|
||||
module.exports = Categories;
|
||||
@@ -0,0 +1,37 @@
|
||||
var BucketsMetadata = require('./buckets');
|
||||
var CategoriesMetadata = require('./categories');
|
||||
var Rule = require('../../../../windshaft-integration/legends/rule.js');
|
||||
|
||||
/**
|
||||
* Generates a list of Metadata objects from the original cartocss_meta rules
|
||||
*
|
||||
* @param {Rules} rulesData
|
||||
* @return {metadata.Base[]}
|
||||
*/
|
||||
function getMetadataFromRules (rulesData) {
|
||||
var metadata = [];
|
||||
|
||||
rulesData.forEach(function (ruleData) {
|
||||
var rule = new Rule(ruleData);
|
||||
|
||||
if (_isBucketsMetadata(rule)) {
|
||||
metadata.push(new BucketsMetadata(rule));
|
||||
} else if (_isCategoriesMetadata(rule)) {
|
||||
metadata.push(new CategoriesMetadata(rule));
|
||||
}
|
||||
});
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function _isBucketsMetadata (rule) {
|
||||
return rule.getBucketsWithRangeFilter().length > 0;
|
||||
}
|
||||
|
||||
function _isCategoriesMetadata (rule) {
|
||||
return rule.getBucketsWithCategoryFilter().length > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMetadataFromRules: getMetadataFromRules
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
function Layers (layers) {
|
||||
this._layers = layers || [];
|
||||
}
|
||||
|
||||
Layers.prototype.add = function (layer) {
|
||||
this._layers.push(layer);
|
||||
return layer;
|
||||
};
|
||||
|
||||
Layers.prototype.remove = function (layer) {
|
||||
return this._layers.splice(this._layers.indexOf(layer), 1);
|
||||
};
|
||||
|
||||
Layers.prototype.size = function () {
|
||||
return this._layers.length;
|
||||
};
|
||||
|
||||
Layers.prototype.indexOf = function (layer) {
|
||||
return this._layers.indexOf(layer);
|
||||
};
|
||||
|
||||
Layers.prototype.contains = function (layer) {
|
||||
return this._layers.indexOf(layer) >= 0;
|
||||
};
|
||||
|
||||
Layers.prototype.findById = function (layerId) {
|
||||
return _.find(this._layers, function (layer) {
|
||||
return layer.getId() === layerId;
|
||||
}, this);
|
||||
};
|
||||
|
||||
Layers.prototype.toArray = function () {
|
||||
return this._layers;
|
||||
};
|
||||
|
||||
Layers.prototype.move = function (layer, toIndex) {
|
||||
var fromIndex = this._layers.indexOf(layer);
|
||||
if (fromIndex >= 0 && fromIndex !== toIndex) {
|
||||
this._layers.splice(toIndex, 0, this._layers.splice(fromIndex, 1)[0]);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Layers;
|
||||
@@ -0,0 +1,70 @@
|
||||
/* global google */
|
||||
var _ = require('underscore');
|
||||
var Layer = require('../layer');
|
||||
var triggerLayerFeatureEvent = require('./trigger-layer-feature-event');
|
||||
var GMapsCartoDBLayerGroupView = require('../../../geo/gmaps/gmaps-cartodb-layer-group-view');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
|
||||
/**
|
||||
* This object is a custom Google Maps MapType to enable feature interactivity
|
||||
* using an internal GMapsCartoDBLayerGroupView instance.
|
||||
*
|
||||
* NOTE: It also contains the feature events handlers. That's why it requires the carto layers array.
|
||||
*/
|
||||
function GoogleMapsMapType (layers, engine, map) {
|
||||
this._layers = layers;
|
||||
this._engine = engine;
|
||||
this._map = map;
|
||||
|
||||
this._hoveredLayers = [];
|
||||
|
||||
this.tileSize = new google.maps.Size(256, 256);
|
||||
this._internalView = new GMapsCartoDBLayerGroupView(this._engine._cartoLayerGroup, {
|
||||
nativeMap: map
|
||||
});
|
||||
this._id = this._internalView._id;
|
||||
this._internalView.on('featureClick', this._onFeatureClick, this);
|
||||
this._internalView.on('featureOver', this._onFeatureOver, this);
|
||||
this._internalView.on('featureOut', this._onFeatureOut, this);
|
||||
this._internalView.on('featureError', this._onFeatureError, this);
|
||||
}
|
||||
|
||||
GoogleMapsMapType.prototype.getTile = function (coord, zoom, ownerDocument) {
|
||||
return this._internalView.getTile(coord, zoom, ownerDocument);
|
||||
};
|
||||
|
||||
GoogleMapsMapType.prototype._onFeatureClick = function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_CLICKED, internalEvent, layer);
|
||||
};
|
||||
|
||||
GoogleMapsMapType.prototype._onFeatureOver = function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
if (layer.isInteractive()) {
|
||||
this._hoveredLayers[internalEvent.layerIndex] = true;
|
||||
this._map.setOptions({ draggableCursor: 'pointer' });
|
||||
}
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_OVER, internalEvent, layer);
|
||||
};
|
||||
|
||||
GoogleMapsMapType.prototype._onFeatureOut = function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
this._hoveredLayers[internalEvent.layerIndex] = false;
|
||||
if (_.any(this._hoveredLayers)) {
|
||||
this._map.setOptions({ draggableCursor: 'pointer' });
|
||||
} else {
|
||||
this._map.setOptions({ draggableCursor: 'auto' });
|
||||
}
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_OUT, internalEvent, layer);
|
||||
};
|
||||
|
||||
GoogleMapsMapType.prototype._onFeatureError = function (error) {
|
||||
var cartoError = new CartoError(error);
|
||||
_.each(this._layers.toArray(), function (layer) {
|
||||
if (layer.isInteractive()) {
|
||||
layer.trigger(Layer.events.TILE_ERROR, cartoError);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = GoogleMapsMapType;
|
||||
@@ -0,0 +1,107 @@
|
||||
/* global L */
|
||||
var _ = require('underscore');
|
||||
var Layer = require('../layer');
|
||||
var constants = require('../constants');
|
||||
var triggerLayerFeatureEvent = require('./trigger-layer-feature-event');
|
||||
var LeafletCartoLayerGroupView = require('../../../geo/leaflet/leaflet-cartodb-layer-group-view');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
|
||||
/**
|
||||
* This object is a custom Leaflet layer to enable feature interactivity
|
||||
* using an internal LeafletCartoLayerGroupView instance.
|
||||
*
|
||||
* There are two overwritten functions:
|
||||
* - addTo: when the layer is added to a map it also creates a LeafletCartoLayerGroupView
|
||||
* object called `_internalView` in order to enable the feature events
|
||||
* - removeFrom: when the layer is removed from a map it also removes the feature events
|
||||
* listeners, triggers a 'remove' event and removes the `_internalView`
|
||||
*
|
||||
* NOTE: It also contains the feature events handlers. That's why it requires the carto layers array.
|
||||
*/
|
||||
var LeafletLayer = L.TileLayer.extend({
|
||||
options: {
|
||||
opacity: 0.99,
|
||||
maxZoom: 30,
|
||||
attribution: constants.ATTRIBUTION
|
||||
},
|
||||
|
||||
initialize: function (layers, engine, options) {
|
||||
_.extend(this.options, options);
|
||||
|
||||
this._layers = layers;
|
||||
this._engine = engine;
|
||||
this._internalView = null;
|
||||
|
||||
this._hoveredLayers = [];
|
||||
},
|
||||
|
||||
addTo: function (map) {
|
||||
if (!this._internalView) {
|
||||
this._internalView = new LeafletCartoLayerGroupView(this._engine._cartoLayerGroup, {
|
||||
nativeMap: map,
|
||||
nativeLayer: this
|
||||
});
|
||||
this._internalView.on('featureClick', this._onFeatureClick, this);
|
||||
this._internalView.on('featureOver', this._onFeatureOver, this);
|
||||
this._internalView.on('featureOut', this._onFeatureOut, this);
|
||||
this._internalView.on('featureError', this._onFeatureError, this);
|
||||
}
|
||||
|
||||
return L.TileLayer.prototype.addTo.call(this, map);
|
||||
},
|
||||
|
||||
removeFrom: function (map) {
|
||||
if (this._internalView) {
|
||||
this._internalView.off('featureClick');
|
||||
this._internalView.off('featureOver');
|
||||
this._internalView.off('featureOut');
|
||||
this._internalView.off('featureError');
|
||||
this._internalView.notifyRemove();
|
||||
}
|
||||
this._internalView = null;
|
||||
|
||||
return L.TileLayer.prototype.removeFrom.call(this, map);
|
||||
},
|
||||
|
||||
setUrl: undefined,
|
||||
|
||||
_setUrl: function (url, noDraw) {
|
||||
return L.TileLayer.prototype.setUrl.call(this, url, noDraw);
|
||||
},
|
||||
|
||||
_onFeatureClick: function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_CLICKED, internalEvent, layer);
|
||||
},
|
||||
|
||||
_onFeatureOver: function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
if (layer.isInteractive()) {
|
||||
this._hoveredLayers[internalEvent.layerIndex] = true;
|
||||
this._map.getContainer().style.cursor = 'pointer';
|
||||
}
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_OVER, internalEvent, layer);
|
||||
},
|
||||
|
||||
_onFeatureOut: function (internalEvent) {
|
||||
var layer = this._layers.findById(internalEvent.layer.id);
|
||||
this._hoveredLayers[internalEvent.layerIndex] = false;
|
||||
if (_.any(this._hoveredLayers)) {
|
||||
this._map.getContainer().style.cursor = 'pointer';
|
||||
} else {
|
||||
this._map.getContainer().style.cursor = 'auto';
|
||||
}
|
||||
triggerLayerFeatureEvent(Layer.events.FEATURE_OUT, internalEvent, layer);
|
||||
},
|
||||
|
||||
_onFeatureError: function (error) {
|
||||
var cartoError = new CartoError(error);
|
||||
_.each(this._layers.toArray(), function (layer) {
|
||||
if (layer.isInteractive()) {
|
||||
layer.trigger(Layer.events.TILE_ERROR, cartoError);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = LeafletLayer;
|
||||
@@ -0,0 +1,57 @@
|
||||
module.exports = function (eventName, internalEvent, layer) {
|
||||
if (layer) {
|
||||
var event = {
|
||||
data: undefined,
|
||||
latLng: undefined
|
||||
};
|
||||
if (internalEvent.feature) {
|
||||
event.data = internalEvent.feature;
|
||||
}
|
||||
if (internalEvent.latlng) {
|
||||
event.latLng = {
|
||||
lat: internalEvent.latlng[0],
|
||||
lng: internalEvent.latlng[1]
|
||||
};
|
||||
}
|
||||
if (internalEvent.position) {
|
||||
event.position = {
|
||||
x: internalEvent.position.x,
|
||||
y: internalEvent.position.y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event object for feature events triggered by {@link carto.layer.Layer}.
|
||||
*
|
||||
* @typedef {object} carto.layer.FeatureEvent
|
||||
* @property {LatLng} latLng - Object with coordinates where interaction took place
|
||||
* @property {object} data - Object with feature data (one attribute for each specified column)
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when user clicks on a feature.
|
||||
*
|
||||
* @event featureClicked
|
||||
* @type {carto.layer.FeatureEvent}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when user moves the mouse over a feature.
|
||||
*
|
||||
* @event featureOver
|
||||
* @type {carto.layer.FeatureEvent}
|
||||
* @api
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when user moves the mouse out of a feature.
|
||||
*
|
||||
* @event featureOut
|
||||
* @type {carto.layer.FeatureEvent}
|
||||
* @api
|
||||
*/
|
||||
layer.trigger(eventName, event);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const CartoError = require('../error-handling/carto-error');
|
||||
const FiltersCollection = require('../filter/filters-collection');
|
||||
const EVENTS = require('../events');
|
||||
|
||||
/**
|
||||
* Base data source object.
|
||||
*
|
||||
* The methods listed in the {@link carto.source.Base|source.Base} object are available in all source objects.
|
||||
*
|
||||
* Use a source to reference the data used in a {@link carto.dataview.Base|dataview} or a {@link carto.layer.Base|layer}.
|
||||
*
|
||||
* {@link carto.source.Base} should not be used directly use {@link carto.source.Dataset} or {@link carto.source.SQL} instead.
|
||||
*
|
||||
* @constructor
|
||||
* @fires error
|
||||
* @abstract
|
||||
* @memberof carto.source
|
||||
* @api
|
||||
*/
|
||||
function Base () {
|
||||
this._id = Base.$generateId();
|
||||
this._hasFiltersApplied = false;
|
||||
this._appliedFilters = new FiltersCollection();
|
||||
}
|
||||
|
||||
_.extend(Base.prototype, Backbone.Events);
|
||||
|
||||
/**
|
||||
* The instance id will be autogenerated by incrementing this variable.
|
||||
*/
|
||||
Base.$nextId = 0;
|
||||
|
||||
/**
|
||||
* Static funciton used internally to autogenerate source ids.
|
||||
*/
|
||||
Base.$generateId = function () {
|
||||
return 'S' + ++Base.$nextId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a unique autogenerated id.
|
||||
*
|
||||
* @return {string} Unique autogenerated id
|
||||
*/
|
||||
Base.prototype.getId = function () {
|
||||
return this._id;
|
||||
};
|
||||
|
||||
Base.prototype._createInternalModel = function (engine) {
|
||||
throw new Error('_createInternalModel must be implemented by the particular source');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire a CartoError event from a internalError
|
||||
*/
|
||||
Base.prototype._triggerError = function (model, internalError) {
|
||||
this.trigger(EVENTS.ERROR, new CartoError(internalError, { analysis: this }));
|
||||
};
|
||||
|
||||
Base.prototype.$setEngine = function (engine) {
|
||||
if (!this._internalModel) {
|
||||
this._internalModel = this._createInternalModel(engine);
|
||||
this._internalModel.on('change:error', this._triggerError, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the engine form the source internal model
|
||||
*/
|
||||
Base.prototype.$getEngine = function (engine) {
|
||||
if (this._internalModel) {
|
||||
return this._internalModel._engine;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the real CARTO.js model used by the source.
|
||||
*/
|
||||
Base.prototype.$getInternalModel = function () {
|
||||
return this._internalModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get added filters
|
||||
*
|
||||
* @returns {Array} Added filters
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.getFilters = function () {
|
||||
return this._appliedFilters.getFilters();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new filter to the source
|
||||
*
|
||||
* @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.addFilter = function (filter) {
|
||||
this._hasFiltersApplied = true;
|
||||
this._appliedFilters.addFilter(filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new filters to the source
|
||||
*
|
||||
* @param {Array<carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR>} filters
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.addFilters = function (filters) {
|
||||
filters.forEach(filter => this.addFilter(filter));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an existing filter from source
|
||||
*
|
||||
* @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.removeFilter = function (filter) {
|
||||
this._appliedFilters.removeFilter(filter);
|
||||
this._hasFiltersApplied = Boolean(this._appliedFilters.count());
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove existing filters from source
|
||||
*
|
||||
* @param {Array<carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR>} filters
|
||||
* @api
|
||||
*/
|
||||
Base.prototype.removeFilters = function (filters) {
|
||||
filters.forEach(filter => this.removeFilter(filter));
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
@@ -0,0 +1,128 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var AnalysisModel = require('../../../analysis/analysis-model');
|
||||
var CamshaftReference = require('../../../analysis/camshaft-reference');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
|
||||
/**
|
||||
* A Dataset that can be used as the data source for layers and dataviews.
|
||||
*
|
||||
* @param {string} tableName The name of an existing table
|
||||
* @example
|
||||
* new carto.source.Dataset('european_cities');
|
||||
* @constructor
|
||||
* @fires error
|
||||
* @extends carto.source.Base
|
||||
* @memberof carto.source
|
||||
* @api
|
||||
*/
|
||||
function Dataset (tableName) {
|
||||
_checkTableName(tableName);
|
||||
this._tableName = tableName;
|
||||
|
||||
Base.apply(this, arguments);
|
||||
|
||||
this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply()));
|
||||
}
|
||||
|
||||
Dataset.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Update the table name. This method is asyncronous and returns a promise which is resolved when the style
|
||||
* is changed succesfully. It also fires a 'tableNameChanged' event.
|
||||
*
|
||||
* @param {string} tableName The name of an existing table
|
||||
* @fires TableNameChanged
|
||||
* @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
|
||||
* @api
|
||||
*/
|
||||
Dataset.prototype.setTableName = function (tableName) {
|
||||
_checkTableName(tableName);
|
||||
|
||||
this._tableName = tableName;
|
||||
|
||||
if (!this._internalModel) {
|
||||
this._triggerTableNameChanged(this, tableName);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this._updateInternalModelQuery(this._getQueryToApply());
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the table name being used in this Dataset object.
|
||||
*
|
||||
* @return {string} The table name being used in this Dataset object
|
||||
* @api
|
||||
*/
|
||||
Dataset.prototype.getTableName = function () {
|
||||
return this._tableName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new internal model with the given engine and attributes initialized in the constructor.
|
||||
*
|
||||
* @param {Engine} engine - The engine object to be assigned to the internalModel
|
||||
*/
|
||||
Dataset.prototype._createInternalModel = function (engine) {
|
||||
var internalModel = new AnalysisModel({
|
||||
id: this.getId(),
|
||||
type: 'source',
|
||||
query: this._getQueryToApply()
|
||||
}, {
|
||||
camshaftReference: CamshaftReference,
|
||||
engine: engine
|
||||
});
|
||||
|
||||
return internalModel;
|
||||
};
|
||||
|
||||
Dataset.prototype._updateInternalModelQuery = function (query) {
|
||||
if (!this._internalModel) return;
|
||||
|
||||
this._internalModel.set('query', query, { silent: true });
|
||||
|
||||
return this._internalModel._engine.reload()
|
||||
.then(() => this._triggerTableNameChanged(this, this._tableName))
|
||||
.catch(windshaftError => Promise.reject(new CartoError(windshaftError)));
|
||||
};
|
||||
|
||||
Dataset.prototype._getQueryToApply = function () {
|
||||
const whereClause = this._appliedFilters.$getSQL();
|
||||
const datasetQuery = `SELECT * from ${this._tableName}`;
|
||||
|
||||
if (_.isEmpty(whereClause)) {
|
||||
return datasetQuery;
|
||||
}
|
||||
|
||||
return `SELECT * FROM (${datasetQuery}) as datasetQuery WHERE ${whereClause}`;
|
||||
};
|
||||
|
||||
Dataset.prototype.addFilter = function (filter) {
|
||||
Base.prototype.addFilter.apply(this, arguments);
|
||||
this._updateInternalModelQuery(this._getQueryToApply());
|
||||
};
|
||||
|
||||
Dataset.prototype.removeFilter = function (filters) {
|
||||
Base.prototype.removeFilter.apply(this, arguments);
|
||||
this._updateInternalModelQuery(this._getQueryToApply());
|
||||
};
|
||||
|
||||
Dataset.prototype._triggerTableNameChanged = function (model, value) {
|
||||
this.trigger('tableNameChanged', value);
|
||||
};
|
||||
|
||||
function _checkTableName (tableName) {
|
||||
if (_.isUndefined(tableName)) {
|
||||
throw new CartoValidationError('source', 'noDatasetName');
|
||||
}
|
||||
if (!_.isString(tableName)) {
|
||||
throw new CartoValidationError('source', 'requiredDatasetString');
|
||||
}
|
||||
if (_.isEmpty(tableName)) {
|
||||
throw new CartoValidationError('source', 'requiredDataset');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dataset;
|
||||
@@ -0,0 +1,11 @@
|
||||
var Dataset = require('./dataset');
|
||||
var SQL = require('./sql');
|
||||
|
||||
/**
|
||||
* @namespace carto.source
|
||||
* @api
|
||||
*/
|
||||
module.exports = {
|
||||
Dataset: Dataset,
|
||||
SQL: SQL
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var AnalysisModel = require('../../../analysis/analysis-model');
|
||||
var CamshaftReference = require('../../../analysis/camshaft-reference');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
|
||||
/**
|
||||
* A SQL Query that can be used as the data source for layers and dataviews.
|
||||
*
|
||||
* @param {string} query A SQL query containing a SELECT statement
|
||||
* @fires error
|
||||
* @example
|
||||
* new carto.source.SQL('SELECT * FROM european_cities');
|
||||
* @constructor
|
||||
* @extends carto.source.Base
|
||||
* @memberof carto.source
|
||||
* @fires queryChanged
|
||||
* @api
|
||||
*/
|
||||
function SQL (query) {
|
||||
_checkQuery(query);
|
||||
this._query = query;
|
||||
|
||||
Base.apply(this, arguments);
|
||||
|
||||
this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply()));
|
||||
}
|
||||
|
||||
SQL.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Update the query. This method is asyncronous and returns a promise which is resolved when the style
|
||||
* is changed succesfully. It also fires a 'queryChanged' event.
|
||||
*
|
||||
* @param {string} query - The sql query that will be the source of the data
|
||||
* @fires queryChanged
|
||||
* @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
|
||||
* @api
|
||||
*/
|
||||
SQL.prototype.setQuery = function (query) {
|
||||
_checkQuery(query);
|
||||
this._query = query;
|
||||
|
||||
const sqlString = this._getQueryToApply();
|
||||
|
||||
if (!this._internalModel) {
|
||||
this._triggerQueryChanged(this, sqlString);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this._updateInternalModelQuery(sqlString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the query being used in this SQL source.
|
||||
*
|
||||
* @return {string} The query being used in this SQL object
|
||||
* @api
|
||||
*/
|
||||
SQL.prototype.getQuery = function () {
|
||||
return this._query;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new internal model with the given engine and attributes initialized in the constructor.
|
||||
*
|
||||
* @param {Engine} engine - The engine object to be assigned to the internalModel
|
||||
*/
|
||||
SQL.prototype._createInternalModel = function (engine) {
|
||||
var internalModel = new AnalysisModel({
|
||||
id: this.getId(),
|
||||
type: 'source',
|
||||
query: this._getQueryToApply()
|
||||
}, {
|
||||
camshaftReference: CamshaftReference,
|
||||
engine: engine
|
||||
});
|
||||
|
||||
internalModel.on('change:query', this._triggerQueryChanged, this);
|
||||
|
||||
return internalModel;
|
||||
};
|
||||
|
||||
SQL.prototype._updateInternalModelQuery = function (query) {
|
||||
if (!this._internalModel) return;
|
||||
|
||||
this._internalModel.set('query', query, { silent: true });
|
||||
|
||||
return this._internalModel._engine.reload()
|
||||
.then(() => this._triggerQueryChanged(this, query))
|
||||
.catch(windshaftError => Promise.reject(new CartoError(windshaftError)));
|
||||
};
|
||||
|
||||
SQL.prototype._getQueryToApply = function () {
|
||||
const whereClause = this._appliedFilters.$getSQL();
|
||||
|
||||
if (!this._hasFiltersApplied || _.isEmpty(whereClause)) {
|
||||
return this._query;
|
||||
}
|
||||
|
||||
return `SELECT * FROM (${this._query}) as originalQuery WHERE ${whereClause}`;
|
||||
};
|
||||
|
||||
SQL.prototype.addFilter = function (filter) {
|
||||
Base.prototype.addFilter.apply(this, arguments);
|
||||
this._updateInternalModelQuery(this._getQueryToApply());
|
||||
};
|
||||
|
||||
SQL.prototype.removeFilter = function (filters) {
|
||||
Base.prototype.removeFilter.apply(this, arguments);
|
||||
this._updateInternalModelQuery(this._getQueryToApply());
|
||||
};
|
||||
|
||||
SQL.prototype._triggerQueryChanged = function (model, value) {
|
||||
this.trigger('queryChanged', value);
|
||||
};
|
||||
|
||||
function _checkQuery (query) {
|
||||
if (!query) {
|
||||
throw new CartoValidationError('source', 'requiredQuery');
|
||||
}
|
||||
|
||||
if (!_.isString(query)) {
|
||||
throw new CartoValidationError('source', 'requiredString');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SQL;
|
||||
|
||||
/**
|
||||
* Fired when the query has changed. Handler gets a parameter with the new query.
|
||||
*
|
||||
* @event queryChanged
|
||||
* @type {string}
|
||||
* @api
|
||||
*/
|
||||
@@ -0,0 +1,33 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Base style object.
|
||||
*
|
||||
* @fires error
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @memberof carto.style
|
||||
* @api
|
||||
*/
|
||||
function Base () {}
|
||||
|
||||
_.extend(Base.prototype, Backbone.Events);
|
||||
|
||||
Base.prototype.$setError = function (cartoError) {
|
||||
this._error = cartoError;
|
||||
this.trigger('error', cartoError);
|
||||
};
|
||||
|
||||
Base.prototype.$setEngine = function (newEngine) {
|
||||
if (this._engine && this._engine !== newEngine) {
|
||||
throw new Error('CartoCSS engine cannot be changed');
|
||||
}
|
||||
this._engine = newEngine;
|
||||
};
|
||||
|
||||
Base.prototype.$getEngine = function () {
|
||||
return this._engine;
|
||||
};
|
||||
|
||||
module.exports = Base;
|
||||
@@ -0,0 +1,96 @@
|
||||
var _ = require('underscore');
|
||||
var Base = require('./base');
|
||||
var CartoValidationError = require('../error-handling/carto-validation-error');
|
||||
var CartoError = require('../error-handling/carto-error');
|
||||
|
||||
// Event constants
|
||||
var CONTENT_CHANGED = 'contentChanged';
|
||||
|
||||
/**
|
||||
* A CartoCSS/TurboCarto style that can be applied to a {@link carto.layer.Layer}.
|
||||
* @param {string} content - A CartoCSS string
|
||||
* @example
|
||||
* var style = new carto.style.CartoCSS(`
|
||||
* #layer {
|
||||
* marker-fill: #FABADA;
|
||||
* marker-width: 10;
|
||||
* }
|
||||
* `);
|
||||
* @constructor
|
||||
* @extends carto.style.Base
|
||||
* @memberof carto.style
|
||||
* @api
|
||||
*/
|
||||
function CartoCSS (content) {
|
||||
_checkContent(content);
|
||||
this._content = content;
|
||||
}
|
||||
|
||||
CartoCSS.prototype = Object.create(Base.prototype);
|
||||
|
||||
/**
|
||||
* Get the current CartoCSS/TurboCarto style as a string.
|
||||
*
|
||||
* @return {string} - The TurboCarto style for this CartoCSS object
|
||||
* @api
|
||||
*/
|
||||
CartoCSS.prototype.getContent = function () {
|
||||
return this._content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the CartoCSS/Turbocarto as a string.
|
||||
*
|
||||
* @param {string} newContent - A string containing the new cartocss/turbocarto style
|
||||
* @return {Promise<string>} A promise that will be resolved once the cartocss/turbocarto is updated
|
||||
* @example
|
||||
* // Get the cartoCSS from an exiting layer
|
||||
* let cartoCSS = layer.getStyle();
|
||||
* // Update the cartoCSS content, remember this method is asynchronous!
|
||||
* cartoCSS.setContent(`
|
||||
* #layer {
|
||||
* marker-fill: blue;
|
||||
* }`)
|
||||
* .then(() => {
|
||||
* console.log('cartoCSS was updated');
|
||||
* })
|
||||
* .catch(() => {
|
||||
* console.error('Error updating the cartoCSS for the layer');
|
||||
* });
|
||||
* @api
|
||||
*/
|
||||
CartoCSS.prototype.setContent = function (newContent) {
|
||||
_checkContent(newContent);
|
||||
this._content = newContent;
|
||||
// Notify layers that the style has been changed so they can update their internalModels.
|
||||
this.trigger('$changed', this);
|
||||
if (!this._engine) {
|
||||
return _onContentChanged.call(this, newContent);
|
||||
}
|
||||
|
||||
return this._engine.reload()
|
||||
.then(function () {
|
||||
return _onContentChanged.call(this, newContent);
|
||||
}.bind(this))
|
||||
.catch(function (windshaftError) {
|
||||
return Promise.reject(new CartoError(windshaftError));
|
||||
});
|
||||
};
|
||||
|
||||
// Once the reload cycle is completed trigger a contentChanged event.
|
||||
function _onContentChanged (newContent) {
|
||||
this.trigger(CONTENT_CHANGED, this._content);
|
||||
return Promise.resolve(this._content);
|
||||
}
|
||||
|
||||
function _checkContent (content) {
|
||||
if (!content) {
|
||||
throw new CartoValidationError('style', 'requiredCSS');
|
||||
}
|
||||
|
||||
if (!_.isString(content)) {
|
||||
throw new CartoValidationError('style', 'requiredCSSString');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CartoCSS;
|
||||
@@ -0,0 +1,9 @@
|
||||
var CartoCSS = require('./cartocss');
|
||||
|
||||
/**
|
||||
* @namespace carto.style
|
||||
* @api
|
||||
*/
|
||||
module.exports = {
|
||||
CartoCSS: CartoCSS
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
var _ = require('underscore');
|
||||
var log = require('cdb.log');
|
||||
var C = require('../constants');
|
||||
|
||||
var VizJSON = function (vizjson) {
|
||||
_.each(Object.keys(vizjson), function (property) {
|
||||
this[property] = vizjson[property];
|
||||
}, this);
|
||||
|
||||
this.overlays = this.overlays || [];
|
||||
this.layers = this.layers || [];
|
||||
|
||||
this._addAttributionOverlay();
|
||||
};
|
||||
|
||||
VizJSON.prototype.isNamedMap = function () {
|
||||
return !!this.datasource.template_name;
|
||||
};
|
||||
|
||||
VizJSON.prototype.hasZoomOverlay = function () {
|
||||
return this.hasOverlay(C.OVERLAY_TYPES.ZOOM);
|
||||
};
|
||||
|
||||
VizJSON.prototype.hasOverlay = function (overlayType) {
|
||||
return _.isObject(this.getOverlayByType(overlayType));
|
||||
};
|
||||
|
||||
VizJSON.prototype.getOverlayByType = function (overlayType) {
|
||||
return _.find(this.overlays, function (overlay) {
|
||||
return overlay.type === overlayType;
|
||||
});
|
||||
};
|
||||
|
||||
VizJSON.prototype.addHeaderOverlay = function (showTitle, showDescription, isShareable) {
|
||||
if (!this.hasOverlay(C.OVERLAY_TYPES.HEADER)) {
|
||||
this.overlays.unshift({
|
||||
type: C.OVERLAY_TYPES.HEADER,
|
||||
order: 1,
|
||||
shareable: isShareable,
|
||||
url: this.url,
|
||||
options: {
|
||||
extra: {
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
show_title: showTitle,
|
||||
show_description: showDescription
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
VizJSON.prototype.addSearchOverlay = function () {
|
||||
if (!this.hasOverlay(C.OVERLAY_TYPES.SEARCH)) {
|
||||
this.overlays.push({
|
||||
type: C.OVERLAY_TYPES.SEARCH,
|
||||
order: 3
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
VizJSON.prototype.removeOverlay = function (overlayType) {
|
||||
for (var i = 0; i < this.overlays.length; ++i) {
|
||||
if (this.overlays[i].type === overlayType) {
|
||||
this.overlays.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VizJSON.prototype.removeLoaderOverlay = function () {
|
||||
this.removeOverlay(C.OVERLAY_TYPES.LOADER);
|
||||
};
|
||||
|
||||
VizJSON.prototype.removeZoomOverlay = function () {
|
||||
this.removeOverlay(C.OVERLAY_TYPES.ZOOM);
|
||||
};
|
||||
|
||||
VizJSON.prototype.removeSearchOverlay = function () {
|
||||
this.removeOverlay(C.OVERLAY_TYPES.SEARCH);
|
||||
};
|
||||
|
||||
VizJSON.prototype.removeLogoOverlay = function (overlayType) {
|
||||
this.removeOverlay(C.OVERLAY_TYPES.LOGO);
|
||||
};
|
||||
|
||||
VizJSON.prototype._addAttributionOverlay = function () {
|
||||
this.overlays.push({
|
||||
type: C.OVERLAY_TYPES.ATTRIBUTION
|
||||
});
|
||||
};
|
||||
|
||||
VizJSON.prototype.enforceGMapsBaseLayer = function (gmapsBaseType, gmapsStyle) {
|
||||
var isGmapsBaseTypeValid = _.contains(C.GMAPS_BASE_LAYER_TYPES, gmapsBaseType);
|
||||
if (this.map_provider === C.MAP_PROVIDER_TYPES.LEAFLET && isGmapsBaseTypeValid) {
|
||||
if (this.layers) {
|
||||
this.layers[0].options.type = 'GMapsBase';
|
||||
this.layers[0].options.baseType = gmapsBaseType;
|
||||
this.layers[0].options.name = gmapsBaseType;
|
||||
|
||||
if (gmapsStyle) {
|
||||
this.layers[0].options.style = typeof gmapsStyle === 'string' ? JSON.parse(gmapsStyle) : gmapsStyle;
|
||||
}
|
||||
|
||||
this.map_provider = C.MAP_PROVIDER_TYPES.GMAPS;
|
||||
this.layers[0].options.attribution = ''; // GMaps has its own attribution
|
||||
} else {
|
||||
log.error('No base map loaded. Using Leaflet.');
|
||||
}
|
||||
} else {
|
||||
log.error('GMaps baseType "' + gmapsBaseType + ' is not supported. Using leaflet.');
|
||||
}
|
||||
};
|
||||
|
||||
VizJSON.prototype.setZoom = function (zoom) {
|
||||
this.zoom = zoom;
|
||||
this.bounds = null;
|
||||
};
|
||||
|
||||
VizJSON.prototype.setCenter = function (center) {
|
||||
this.center = center;
|
||||
this.bounds = null;
|
||||
};
|
||||
|
||||
VizJSON.prototype.setBounds = function (bounds) {
|
||||
this.bounds = bounds;
|
||||
};
|
||||
|
||||
VizJSON.prototype.setVector = function (vector) {
|
||||
this.vector = vector;
|
||||
};
|
||||
|
||||
module.exports = VizJSON;
|
||||
Reference in New Issue
Block a user