826 lines
23 KiB
JavaScript
826 lines
23 KiB
JavaScript
|
cdb.admin.CartoDBTableData = cdb.ui.common.TableData.extend({
|
||
|
_ADDED_ROW_TEXT: 'Row added correctly',
|
||
|
_ADDING_ROW_TEXT: 'Adding a new row',
|
||
|
_GEOMETRY_UPDATED: 'Table geometry updated',
|
||
|
|
||
|
model: cdb.admin.Row,
|
||
|
|
||
|
initialize: function(models, options) {
|
||
|
var self = this;
|
||
|
this.table = options ? options.table: null;
|
||
|
this.model.prototype.idAttribute = 'cartodb_id';
|
||
|
this.initOptions();
|
||
|
this.filter = null;
|
||
|
this._fetching = false;
|
||
|
this.pages = [];
|
||
|
this.lastPage = false;
|
||
|
this.bind('newPage', this.newPage, this);
|
||
|
this.bind('reset', function() {
|
||
|
var pages = Math.floor(this.size()/this.options.get('rows_per_page'))
|
||
|
this.pages = [];
|
||
|
|
||
|
for(var i = 0; i < pages; ++i) {
|
||
|
this.pages.push(i);
|
||
|
}
|
||
|
}, this);
|
||
|
|
||
|
if(this.table) {
|
||
|
this.bind('add change:the_geom', function(row) {
|
||
|
var gt = this.table.get('geometry_types')
|
||
|
if(gt && gt.length > 0) return;
|
||
|
if(row.get('the_geom')) {
|
||
|
// we set it to silent because a change in geometry_types
|
||
|
// raises rendering and column feching
|
||
|
this.table.addGeomColumnType(row.getGeomType());
|
||
|
}
|
||
|
}, this);
|
||
|
}
|
||
|
this.elder('initialize');
|
||
|
},
|
||
|
|
||
|
initOptions: function() {
|
||
|
var self = this;
|
||
|
this.options = new cdb.core.Model({
|
||
|
rows_per_page:40,
|
||
|
page: 0,
|
||
|
sort_order: 'asc',
|
||
|
order_by: 'cartodb_id',
|
||
|
filter_column: '',
|
||
|
filter_value: ''
|
||
|
});
|
||
|
this.options.bind('change', function() {
|
||
|
if(self._fetching) {
|
||
|
return;
|
||
|
}
|
||
|
self._fetching = true;
|
||
|
opt = {};
|
||
|
var previous = this.options.previous('page');
|
||
|
|
||
|
if(this.options.hasChanged('page')) {
|
||
|
opt.add = true;
|
||
|
opt.changingPage = true;
|
||
|
// if user is going backwards insert new rows at the top
|
||
|
if(previous > this.options.get('page')) {
|
||
|
opt.at = 0;
|
||
|
}
|
||
|
} else {
|
||
|
if(this.options.hasChanged('mode')) {
|
||
|
this.options.set({
|
||
|
'page': 0
|
||
|
}, { silent: true });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
opt.success = function(_coll, resp) {
|
||
|
self.trigger('loaded');
|
||
|
if(resp.rows && resp.rows.length !== 0) {
|
||
|
if(opt.changingPage) {
|
||
|
self.trigger('newPage', self.options.get('page'), opt.at === 0? 'up': 'down');
|
||
|
}
|
||
|
} else {
|
||
|
// no data so do not change the page
|
||
|
self.options.set({page: previous});//, { silent: true });
|
||
|
}
|
||
|
self.trigger('endLoadingRows', self.options.get('page'), opt.at === 0? 'up': 'down');
|
||
|
self._fetching = false;
|
||
|
};
|
||
|
|
||
|
opt.error = function() {
|
||
|
cdb.log.error("there was some problem fetching rows");
|
||
|
self.trigger('endLoadingRows');
|
||
|
self._fetching = false;
|
||
|
};
|
||
|
|
||
|
self.trigger('loadingRows', opt.at === 0 ? 'up': 'down');
|
||
|
this.fetch(opt);
|
||
|
|
||
|
}, this);
|
||
|
},
|
||
|
|
||
|
parse: function(d) {
|
||
|
// when the query modifies the data modified flag is true
|
||
|
// TODO: change this when SQL API was able to say if a
|
||
|
// query modify some data
|
||
|
// HACK, it will fail if using returning sql statement
|
||
|
this.modify_rows = d.rows.length === 0 && _.size(d.fields) === 0;
|
||
|
this.affected_rows = d.affected_rows;
|
||
|
this.lastPage = false;
|
||
|
if(d.rows.length < this.options.get('rows_per_page')) {
|
||
|
this.lastPage = true;
|
||
|
}
|
||
|
return d.rows;
|
||
|
},
|
||
|
|
||
|
// given fields array as they come from SQL create a map name -> type
|
||
|
_schemaFromQueryFields: function(fields) {
|
||
|
var sc = {};
|
||
|
for(k in fields) {
|
||
|
sc[k] = fields[k].type;
|
||
|
}
|
||
|
return sc;
|
||
|
},
|
||
|
|
||
|
_createUrlOptions: function(filter) {
|
||
|
var attr;
|
||
|
if (filter) {
|
||
|
var a = {}
|
||
|
for (var k in this.options.attributes) {
|
||
|
if (filter(k)) {
|
||
|
a[k] = this.options.attributes[k];
|
||
|
}
|
||
|
}
|
||
|
attr = _(a);
|
||
|
} else {
|
||
|
attr = _(this.options.attributes);
|
||
|
}
|
||
|
var params = attr.map(function(v, k) {
|
||
|
return k + "=" + encodeURIComponent(v);
|
||
|
}).join('&');
|
||
|
params += "&api_key=" + cdb.config.get('api_key');
|
||
|
return params;
|
||
|
},
|
||
|
|
||
|
_geometryColumnSQL: function(c) {
|
||
|
return [
|
||
|
"CASE",
|
||
|
"WHEN GeometryType(" + c + ") = 'POINT' THEN",
|
||
|
"ST_AsGeoJSON(" + c + ",8)",
|
||
|
"WHEN (" + c + " IS NULL) THEN",
|
||
|
"NULL",
|
||
|
"ELSE",
|
||
|
"GeometryType(" + c + ")",
|
||
|
"END " + c
|
||
|
].join(' ');
|
||
|
},
|
||
|
|
||
|
// return wrapped SQL removing the_geom and the_geom_webmercator
|
||
|
// to avoid fetching those columns.
|
||
|
// So for a sql like
|
||
|
// select * from table the returned value is
|
||
|
// select column1, column2, column3... from table
|
||
|
wrappedSQL: function(schema, exclude, fetchGeometry) {
|
||
|
var self = this;
|
||
|
exclude = exclude || ['the_geom_webmercator'];
|
||
|
schema = _.clone(schema);
|
||
|
|
||
|
var select_columns = _.chain(schema).omit(exclude).map(function(v, k) {
|
||
|
if (v === 'geometry') {
|
||
|
if(fetchGeometry) {
|
||
|
return "st_astext(\"" + k + "\") " + "as " + k
|
||
|
}
|
||
|
return self._geometryColumnSQL(k);
|
||
|
}
|
||
|
return '"' + k + '"';
|
||
|
}).value();
|
||
|
|
||
|
select_columns = select_columns.join(',');
|
||
|
|
||
|
var mode = this.options.get('sort_order') === 'desc' ? 'desc': 'asc';
|
||
|
|
||
|
var q = "select " + select_columns + " from (" + this.getSQL() + ") __wrapped";
|
||
|
var order_by = this.options.get('order_by');
|
||
|
if (order_by && order_by.length > 0) {
|
||
|
q += " order by " + order_by + " " + mode;
|
||
|
}
|
||
|
return q;
|
||
|
|
||
|
},
|
||
|
|
||
|
url: function() {
|
||
|
return this.sqlApiUrl();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* we need to override sync to avoid the sql request to be sent by GET.
|
||
|
* For security reasons, we need them to be send as a PUT request.
|
||
|
* @method sync
|
||
|
* @param method {'save' || 'read' || 'delete' || 'create'}
|
||
|
* @param model {Object}
|
||
|
* @param options {Object}
|
||
|
*/
|
||
|
sync: function(method, model, options) {
|
||
|
if(!options) { options = {}; }
|
||
|
options.data = this._createUrlOptions(function(d) {
|
||
|
return d !== 'sql';
|
||
|
});
|
||
|
|
||
|
if (cdb.admin.CartoDBTableMetadata.alterTableData(this.options.get('sql') || '')) {
|
||
|
options.data += "&q=" + encodeURIComponent(this.options.get('sql'));
|
||
|
options.type = 'POST';
|
||
|
} else {
|
||
|
// when a geometry can be lazy fetched, don't fetch it
|
||
|
var fetchGeometry = 'cartodb_id' in this.query_schema;
|
||
|
options.data += "&q=" + encodeURIComponent(this.wrappedSQL(this.query_schema, [], !fetchGeometry));
|
||
|
|
||
|
if (options.data.length > 2048) {
|
||
|
options.type = 'POST';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Backbone.sync.call(this, method, this, options);
|
||
|
},
|
||
|
|
||
|
sqlApiUrl: function() {
|
||
|
return cdb.config.getSqlApiUrl();
|
||
|
},
|
||
|
|
||
|
setOptions: function(opt) {
|
||
|
this.options.set(opt);
|
||
|
},
|
||
|
|
||
|
// Refresh all table data
|
||
|
refresh: function() {
|
||
|
this.fetch();
|
||
|
},
|
||
|
|
||
|
isFetchingPage: function() {
|
||
|
return this._fetching;
|
||
|
},
|
||
|
|
||
|
loadPageAtTop: function() {
|
||
|
|
||
|
if(!this._fetching) {
|
||
|
var first = this.pages[0];
|
||
|
|
||
|
if(first > 0) {
|
||
|
this.options.set('page', first - 1);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
loadPageAtBottom: function() {
|
||
|
if(!this._fetching) {
|
||
|
var last = this.pages[this.pages.length - 1];
|
||
|
|
||
|
if(!this.lastPage) {
|
||
|
this.options.set('page', last + 1);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* called when a new page is loaded
|
||
|
* removes the models to max
|
||
|
*/
|
||
|
newPage: function(currentPage, direction) {
|
||
|
if(this.pages.indexOf(currentPage) < 0) {
|
||
|
this.pages.push(currentPage);
|
||
|
};
|
||
|
this.pages.sort(function(a, b) {
|
||
|
return Number(a) > Number(b);
|
||
|
});
|
||
|
// remove blocks if there are more rows than allowed
|
||
|
var rowspp = this.options.get('rows_per_page');
|
||
|
var max_items = rowspp*4;
|
||
|
if(this.size() > max_items) {
|
||
|
if(direction == 'up') {
|
||
|
// remove page from the bottom (the user is going up)
|
||
|
this.pages.pop();
|
||
|
this.remove(this.models.slice(max_items, this.size()));
|
||
|
} else {
|
||
|
// remove page from the top (the user is going down)
|
||
|
this.pages.shift();
|
||
|
this.remove(this.models.slice(0, rowspp));
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/*setPage: function(p) {
|
||
|
if(!this._fetching && p >= 0) {
|
||
|
this.setOptions({page: p});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
getPage: function(p) {
|
||
|
return this.options.get('page');
|
||
|
},*/
|
||
|
addRow: function(opts) {
|
||
|
var self = this;
|
||
|
this.table.notice(self._ADDING_ROW_TEXT, 'load', 0)
|
||
|
var self = this;
|
||
|
opts = opts || {};
|
||
|
_.extend(opts, {
|
||
|
wait: true,
|
||
|
success: function() {
|
||
|
self.table.notice(self._ADDED_ROW_TEXT)
|
||
|
},
|
||
|
error: function(e, resp) {
|
||
|
//TODO: notice user
|
||
|
self.table.error(self._ADDING_ROW_TEXT, resp);
|
||
|
}
|
||
|
});
|
||
|
return this.create(null, opts);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* creates a new row model in local, it is NOT serialized to the server
|
||
|
*/
|
||
|
newRow: function(attrs) {
|
||
|
r = new cdb.admin.Row(attrs);
|
||
|
r.table = this.table;
|
||
|
r.bind('saved', function _saved() {
|
||
|
if(r.table.data().length == 0) {
|
||
|
r.table.data().fetch();
|
||
|
r.unbind('saved', _saved, r.table);
|
||
|
}
|
||
|
}, r.table);
|
||
|
return r;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* return a model row
|
||
|
*/
|
||
|
getRow: function(id, options) {
|
||
|
options = options || {};
|
||
|
var r = this.get(id);
|
||
|
if(!r) {
|
||
|
r = new cdb.admin.Row({cartodb_id: id});
|
||
|
}
|
||
|
if(!options.no_add) {
|
||
|
this.table._data.add(r);
|
||
|
}
|
||
|
r.table = this.table;
|
||
|
return r;
|
||
|
},
|
||
|
|
||
|
getRowAt: function(index) {
|
||
|
var r = this.at(index);
|
||
|
r.table = this.table;
|
||
|
return r;
|
||
|
},
|
||
|
|
||
|
deleteRow: function(row_id) {
|
||
|
},
|
||
|
|
||
|
isReadOnly: function() {
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
quartiles: function(nslots, column, callback, error) {
|
||
|
var tmpl = _.template('select quartile, max(<%= column %>) as maxamount from (select <%= column %>, ntile(<%= slots %>) over (order by <%= column %>) as quartile from (<%= sql %>) _table_sql where <%= column %> is not null) x group by quartile order by quartile');
|
||
|
this._sqlQuery(tmpl({
|
||
|
slots: nslots,
|
||
|
sql: this.getSQL(),
|
||
|
column: column
|
||
|
}),
|
||
|
function(data) {
|
||
|
callback(_(data.rows).pluck('maxamount'));
|
||
|
},
|
||
|
error);
|
||
|
},
|
||
|
|
||
|
equalInterval: function(nslots, column, callback, error) {
|
||
|
var tmpl = _.template(' \
|
||
|
with params as (select min(a), max(a) from ( select <%= column %> as a from (<%= sql %>) _table_sql where <%= column %> is not null ) as foo ) \
|
||
|
select (max-min)/<%= slots %> as s, min, max from params'
|
||
|
);
|
||
|
this._sqlQuery(tmpl({
|
||
|
slots: nslots,
|
||
|
sql: this.getSQL(),
|
||
|
column: column
|
||
|
}),
|
||
|
function(data) {
|
||
|
var min = data.rows[0].min;
|
||
|
var max = data.rows[0].max;
|
||
|
var range = data.rows[0].s;
|
||
|
var values = [];
|
||
|
|
||
|
for (var i = 1, l = nslots; i < l; i++) {
|
||
|
values.push((range*i) + min);
|
||
|
}
|
||
|
|
||
|
// Add last value
|
||
|
values.push(max);
|
||
|
// Callback
|
||
|
callback(values);
|
||
|
},
|
||
|
error);
|
||
|
},
|
||
|
|
||
|
_quantificationMethod: function(functionName, nslots, column, distinct, callback, error) {
|
||
|
var tmpl = _.template('select unnest(<%= functionName %>(array_agg(<%= simplify_fn %>((<%= column %>::numeric))), <%= slots %>)) as buckets from (<%= sql %>) _table_sql where <%= column %> is not null');
|
||
|
this._sqlQuery(tmpl({
|
||
|
slots: nslots,
|
||
|
sql: this.getSQL(),
|
||
|
column: column,
|
||
|
functionName: functionName,
|
||
|
simplify_fn: 'distinct'
|
||
|
}),
|
||
|
function(data) {
|
||
|
callback(_(data.rows).pluck('buckets'));
|
||
|
},
|
||
|
error);
|
||
|
},
|
||
|
|
||
|
discreteHistogram: function(nbuckets, column, callback, error) {
|
||
|
|
||
|
var query = 'SELECT DISTINCT(<%= column %>) AS bucket, count(*) AS value FROM (<%= sql %>) _table_sql GROUP BY <%= column %> ORDER BY value DESC LIMIT <%= nbuckets %> + 1';
|
||
|
|
||
|
var sql = _.template( query, {
|
||
|
column: column,
|
||
|
nbuckets: nbuckets,
|
||
|
sql: this.getSQL(),
|
||
|
});
|
||
|
|
||
|
this._sqlQuery(sql, function(data) {
|
||
|
|
||
|
var count = data.rows.length;
|
||
|
var reached_limit = false;
|
||
|
|
||
|
if (count > nbuckets) {
|
||
|
data.rows = data.rows.slice(0, nbuckets);
|
||
|
reached_limit = true;
|
||
|
}
|
||
|
|
||
|
callback({ rows: data.rows, reached_limit: reached_limit });
|
||
|
|
||
|
});
|
||
|
|
||
|
},
|
||
|
|
||
|
date_histogram: function(nbuckets, column, callback, error) {
|
||
|
|
||
|
column = "EXTRACT(EPOCH FROM " + column + "::TIMESTAMP WITH TIME ZONE )";
|
||
|
|
||
|
var tmpl = _.template(
|
||
|
'with bounds as ( ' +
|
||
|
'SELECT ' +
|
||
|
'current_timestamp as tz, ' +
|
||
|
'min(<%= column %>) as lower, ' +
|
||
|
'max(<%= column %>) as upper, ' +
|
||
|
'(max(<%= column %>) - min(<%= column %>)) as span, ' +
|
||
|
'CASE WHEN ABS((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>) <= 0 THEN 1 ELSE GREATEST(1.0, pow(10,ceil(log((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>)))) END as bucket_size ' +
|
||
|
'FROM (<%= sql %>) _table_sql ' +
|
||
|
') ' +
|
||
|
'select array_agg(v) val, array_agg(bucket) buckets, tz, bounds.upper, bounds.lower, bounds.span, bounds.bucket_size from ' +
|
||
|
'( ' +
|
||
|
'select ' +
|
||
|
'count(<%= column %>) as v, ' +
|
||
|
'round((<%= column %> - bounds.lower)/bounds.bucket_size) as bucket ' +
|
||
|
'from (<%= sql %>) _table_sql, bounds ' +
|
||
|
'where <%= column %> is not null ' +
|
||
|
'group by bucket order by bucket ' +
|
||
|
') a, bounds ' +
|
||
|
'group by ' +
|
||
|
'bounds.upper, bounds.lower, bounds.span, bounds.bucket_size, bounds.tz ');
|
||
|
|
||
|
// transform array_agg from postgres to a js array
|
||
|
function agg_array(a) {
|
||
|
return a.map(function(v) { return parseFloat(v) });
|
||
|
}
|
||
|
|
||
|
this._sqlQuery(tmpl({
|
||
|
nbuckets: nbuckets,
|
||
|
sql: this.getSQL(),
|
||
|
column: column
|
||
|
}),
|
||
|
|
||
|
function(data) {
|
||
|
|
||
|
if (!data.rows || data.rows.length === 0) {
|
||
|
callback(null, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var data = data.rows[0];
|
||
|
data.val = agg_array(data.val);
|
||
|
data.buckets = agg_array(data.buckets);
|
||
|
|
||
|
var hist = [];
|
||
|
var bounds = {};
|
||
|
|
||
|
// create a sorted array and normalize
|
||
|
var upper = data.upper;
|
||
|
var lower = data.lower;
|
||
|
var span = data.span;
|
||
|
var tz = data.tz;
|
||
|
var bucket_size = data.bucket_size
|
||
|
var max, min;
|
||
|
|
||
|
max = data.val[0];
|
||
|
|
||
|
for (var r = 0; r < data.buckets.length; ++r) {
|
||
|
var b = data.buckets[r];
|
||
|
var v = hist[b] = data.val[r];
|
||
|
max = Math.max(max, v);
|
||
|
}
|
||
|
|
||
|
//var maxBucket = _.max(data.buckets)
|
||
|
for (var i = 0; i < hist.length; ++i) {
|
||
|
if (hist[i] === undefined) {
|
||
|
hist[i] = 0;
|
||
|
} else {
|
||
|
hist[i] = hist[i]/max;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bounds.upper = parseFloat(upper);
|
||
|
bounds.lower = parseFloat(lower);
|
||
|
bounds.bucket_size = parseFloat(bucket_size)
|
||
|
bounds.tz = tz;
|
||
|
|
||
|
callback(hist, bounds);
|
||
|
|
||
|
},
|
||
|
|
||
|
error);
|
||
|
},
|
||
|
|
||
|
histogram: function(nbuckets, column, callback, error) {
|
||
|
|
||
|
var tmpl = _.template(
|
||
|
'with bounds as ( ' +
|
||
|
'SELECT ' +
|
||
|
'min(<%= column %>) as lower, ' +
|
||
|
'max(<%= column %>) as upper, ' +
|
||
|
'(max(<%= column %>) - min(<%= column %>)) as span, ' +
|
||
|
'CASE WHEN ABS((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>) <= 0 THEN 1 ELSE GREATEST(1.0, pow(10,ceil(log((max(<%= column %>) - min(<%= column %>))/<%= nbuckets %>)))) END as bucket_size ' +
|
||
|
'FROM (<%= sql %>) _table_sql ' +
|
||
|
') ' +
|
||
|
'select array_agg(v) val, array_agg(bucket) buckets, bounds.upper, bounds.lower, bounds.span, bounds.bucket_size from ' +
|
||
|
'( ' +
|
||
|
'select ' +
|
||
|
'count(<%= column %>) as v, ' +
|
||
|
'round((<%= column %> - bounds.lower)/bounds.bucket_size) as bucket ' +
|
||
|
'from (<%= sql %>) _table_sql, bounds ' +
|
||
|
'where <%= column %> is not null ' +
|
||
|
'group by bucket order by bucket ' +
|
||
|
') a, bounds ' +
|
||
|
'group by ' +
|
||
|
'bounds.upper, ' +
|
||
|
'bounds.lower, bounds.span, bounds.bucket_size ');
|
||
|
|
||
|
// transform array_agg from postgres to a js array
|
||
|
function agg_array(a) {
|
||
|
return a.map(function(v) { return parseFloat(v) });
|
||
|
//return JSON.parse(a.replace('{', '[').replace('}', ']'))
|
||
|
}
|
||
|
|
||
|
this._sqlQuery(tmpl({
|
||
|
nbuckets: nbuckets,
|
||
|
sql: this.getSQL(),
|
||
|
column: column
|
||
|
}),
|
||
|
|
||
|
function(data) {
|
||
|
|
||
|
if(!data.rows || data.rows.length === 0) {
|
||
|
callback(null, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var data = data.rows[0];
|
||
|
|
||
|
data.val = agg_array(data.val);
|
||
|
data.buckets = agg_array(data.buckets);
|
||
|
|
||
|
var hist = [];
|
||
|
var bounds = {};
|
||
|
|
||
|
// create a sorted array and normalize
|
||
|
var upper = data.upper;
|
||
|
var lower = data.lower;
|
||
|
var span = data.span;
|
||
|
var bucket_size = data.bucket_size
|
||
|
var max, min;
|
||
|
|
||
|
max = data.val[0];
|
||
|
|
||
|
for(var r = 0; r < data.buckets.length; ++r) {
|
||
|
var b = data.buckets[r];
|
||
|
var v = hist[b] = data.val[r];
|
||
|
max = Math.max(max, v);
|
||
|
}
|
||
|
|
||
|
|
||
|
//var maxBucket = _.max(data.buckets)
|
||
|
for (var i = 0; i < hist.length; ++i) {
|
||
|
if (hist[i] === undefined) {
|
||
|
hist[i] = 0;
|
||
|
} else {
|
||
|
hist[i] = hist[i]/max;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bounds.upper = parseFloat(upper);
|
||
|
bounds.lower = parseFloat(lower);
|
||
|
bounds.bucket_size = parseFloat(bucket_size)
|
||
|
|
||
|
callback(hist, bounds);
|
||
|
|
||
|
},
|
||
|
|
||
|
error);
|
||
|
},
|
||
|
|
||
|
jenkBins: function(nslots, column, callback, error) {
|
||
|
this._quantificationMethod('CDB_JenksBins', nslots, column, true, callback, error);
|
||
|
},
|
||
|
|
||
|
headTails: function(nslots, column, callback, error) {
|
||
|
this._quantificationMethod('CDB_HeadsTailsBins', nslots, column, false, callback, error);
|
||
|
},
|
||
|
|
||
|
quantileBins: function(nslots, column, callback, error) {
|
||
|
this._quantificationMethod('CDB_QuantileBins', nslots, column, false, callback, error);
|
||
|
},
|
||
|
|
||
|
categoriesForColumn: function(max_values, column, callback, error) {
|
||
|
|
||
|
var tmpl = _.template('\
|
||
|
SELECT <%= column %>, count(<%= column %>) FROM (<%= sql %>) _table_sql ' +
|
||
|
'GROUP BY <%= column %> ORDER BY count DESC LIMIT <%= max_values %> '
|
||
|
);
|
||
|
|
||
|
this._sqlQuery(tmpl({
|
||
|
sql: this.getSQL(),
|
||
|
column: column,
|
||
|
max_values: max_values + 1
|
||
|
}),
|
||
|
function(data) {
|
||
|
callback({
|
||
|
type: data.fields[column].type || 'string',
|
||
|
categories: _(data.rows).pluck(column)
|
||
|
});
|
||
|
},
|
||
|
error);
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* call callback with the geometry bounds
|
||
|
*/
|
||
|
geometryBounds: function(callback) {
|
||
|
var tmpl = _.template('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 %>) _table_sql');
|
||
|
this._sqlQuery(tmpl({
|
||
|
sql: this.getSQL()
|
||
|
}),
|
||
|
function(result) {
|
||
|
var coordinates = result.rows[0];
|
||
|
|
||
|
var lon0 = coordinates.maxx;
|
||
|
var lat0 = coordinates.maxy;
|
||
|
var lon1 = coordinates.minx;
|
||
|
var lat1 = coordinates.miny;
|
||
|
|
||
|
var minlat = -85.0511;
|
||
|
var maxlat = 85.0511;
|
||
|
var minlon = -179;
|
||
|
var maxlon = 179;
|
||
|
|
||
|
var clampNum = function(x, min, max) {
|
||
|
return x < min ? min : x > max ? max : x;
|
||
|
};
|
||
|
|
||
|
lon0 = clampNum(lon0, minlon, maxlon);
|
||
|
lon1 = clampNum(lon1, minlon, maxlon);
|
||
|
lat0 = clampNum(lat0, minlat, maxlat);
|
||
|
lat1 = clampNum(lat1, minlat, maxlat);
|
||
|
callback([ [lat0, lon0], [lat1, lon1]]);
|
||
|
}
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_sqlQuery: function(sql, callback, error, type) {
|
||
|
var s = encodeURIComponent(sql);
|
||
|
return $.ajax({
|
||
|
type: type || "POST",
|
||
|
data: "q=" + s + "&api_key=" + cdb.config.get('api_key'),
|
||
|
url: this.url(),
|
||
|
success: callback,
|
||
|
error: error
|
||
|
});
|
||
|
},
|
||
|
|
||
|
getSQL: function() {
|
||
|
// use table.id to fetch data because if always contains the real table name
|
||
|
return 'select * from ' + cdb.Utils.safeTableNameQuoting(this.table.get('id'));
|
||
|
},
|
||
|
|
||
|
fetch: function(opts) {
|
||
|
var self = this;
|
||
|
opts = opts || {};
|
||
|
if(!opts || !opts.add) {
|
||
|
this.options.attributes.page = 0;
|
||
|
this.options._previousAttributes.page = 0;
|
||
|
this.pages = [];
|
||
|
}
|
||
|
var error = opts.error;
|
||
|
opts.error = function(model, resp) {
|
||
|
self.fetched = true;
|
||
|
self.trigger('error', model, resp);
|
||
|
error && error(model, resp);
|
||
|
}
|
||
|
var success = opts.success;
|
||
|
opts.success = function(model, resp) {
|
||
|
self.fetched = true;
|
||
|
success && success.apply(self, arguments);
|
||
|
}
|
||
|
this._fetch(opts);
|
||
|
},
|
||
|
|
||
|
_fetch: function(opts) {
|
||
|
var MAX_GET_LENGTH = 1024;
|
||
|
var self = this;
|
||
|
this.trigger('loading', opts);
|
||
|
|
||
|
var sql = this.getSQL();
|
||
|
// if the query changes the database just send it
|
||
|
if (cdb.admin.CartoDBTableMetadata.alterTableData(sql)) {
|
||
|
cdb.ui.common.TableData.prototype.fetch.call(self, opts);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// use get to fetch the schema, probably cached
|
||
|
this._sqlQuery(_.template('select * from (<%= sql %>) __wrapped limit 0')({ sql: sql }), function(data) {
|
||
|
// get schema
|
||
|
self.query_schema = self._schemaFromQueryFields(data.fields);
|
||
|
if (!self.table.isInSQLView()) {
|
||
|
if ('the_geom' in self.query_schema) {
|
||
|
delete self.query_schema['the_geom_webmercator'];
|
||
|
}
|
||
|
}
|
||
|
cdb.ui.common.TableData.prototype.fetch.call(self, opts);
|
||
|
}, function (err) {
|
||
|
self.trigger('error', self, err);
|
||
|
}, sql.length > MAX_GET_LENGTH ? "POST" : "GET");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* with the data from the rows fetch create an schema
|
||
|
* if the schema from original table is passed the method
|
||
|
* set the column types according to it
|
||
|
* return an empty list if no data was fetch
|
||
|
*/
|
||
|
schemaFromData: function(originalTableSchema) {
|
||
|
// build schema in format [ [field, type] , ...]
|
||
|
return cdb.admin.CartoDBTableMetadata.sortSchema(_(this.query_schema).map(function(v, k) {
|
||
|
return [k, v];
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
geometryTypeFromGeoJSON: function(geojson) {
|
||
|
try {
|
||
|
var geo = JSON.parse(geojson);
|
||
|
return geo.type
|
||
|
} catch(e) {
|
||
|
}
|
||
|
},
|
||
|
|
||
|
geometryTypeFromWKT: function(wkt) {
|
||
|
if(!wkt) return null;
|
||
|
var types = cdb.admin.WKT.types;
|
||
|
wkt = wkt.toUpperCase();
|
||
|
for(var i = 0; i < types.length; ++i) {
|
||
|
var t = types[i];
|
||
|
if (wkt.indexOf(t) !== -1) {
|
||
|
return t;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
geometryTypeFromWKB: function(wkb) {
|
||
|
if(!wkb) return null;
|
||
|
|
||
|
var typeMap = {
|
||
|
'0001': 'Point',
|
||
|
'0002': 'LineString',
|
||
|
'0003': 'Polygon',
|
||
|
'0004': 'MultiPoint',
|
||
|
'0005': 'MultiLineString',
|
||
|
'0006': 'MultiPolygon'
|
||
|
};
|
||
|
|
||
|
var bigendian = wkb[0] === '0' && wkb[1] === '0';
|
||
|
var type = wkb.substring(2, 6);
|
||
|
if(!bigendian) {
|
||
|
// swap '0100' => '0001'
|
||
|
type = type[2] + type[3] + type[0] + type[1];
|
||
|
}
|
||
|
return typeMap[type];
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
//
|
||
|
// guesses from the first row the geometry types involved
|
||
|
// returns an empty array where there is no rows
|
||
|
// return postgist types, like st_GEOTYPE
|
||
|
//
|
||
|
getGeometryTypes: function() {
|
||
|
var row = null;
|
||
|
var i = this.size();
|
||
|
while (i-- && !(row && row.get('the_geom'))) {
|
||
|
row = this.at(i);
|
||
|
}
|
||
|
if(!row) return [];
|
||
|
var geom = row.get('the_geom') || row.get('the_geom_webmercator');
|
||
|
var geoType = this.geometryTypeFromWKB(geom) || this.geometryTypeFromWKT(geom);
|
||
|
if(geoType) {
|
||
|
return ['ST_' + geoType[0].toUpperCase() + geoType.substring(1).toLowerCase()];
|
||
|
}
|
||
|
return [];
|
||
|
},
|
||
|
|
||
|
});
|