526 lines
16 KiB
JavaScript
526 lines
16 KiB
JavaScript
|
cdb.admin.CartoDBLayer = cdb.geo.CartoDBLayer.extend({
|
||
|
MAX_HISTORY: 5,
|
||
|
MAX_HISTORY_QUERY: 5,
|
||
|
MAX_HISTORY_TILE_STYLE: 5,
|
||
|
|
||
|
initialize: function() {
|
||
|
this.sync = _.debounce(this.sync, 1000);
|
||
|
this.error = false;
|
||
|
|
||
|
this.set('use_server_style', true);
|
||
|
|
||
|
this.initHistory('query');
|
||
|
this.initHistory('tile_style');
|
||
|
|
||
|
this.table = new cdb.admin.CartoDBTableMetadata({
|
||
|
id: this.get('table_name')
|
||
|
});
|
||
|
|
||
|
this.infowindow = new cdb.geo.ui.InfowindowModel({
|
||
|
template_name: 'infowindow_light'
|
||
|
});
|
||
|
|
||
|
this.tooltip = new cdb.geo.ui.InfowindowModel({
|
||
|
template_name: 'tooltip_light'
|
||
|
});
|
||
|
|
||
|
var wizard_properties = this.get('wizard_properties');
|
||
|
|
||
|
this.wizard_properties = new cdb.admin.WizardProperties(_.extend({},
|
||
|
wizard_properties, {
|
||
|
table: this.table,
|
||
|
layer: this
|
||
|
}
|
||
|
));
|
||
|
|
||
|
//this.wizard_properties.properties(wizard_properties);
|
||
|
|
||
|
this.wizard_properties.bind('change:type', this._manageInteractivity, this);
|
||
|
|
||
|
this.legend = new cdb.geo.ui.LegendModel();
|
||
|
|
||
|
// Bindings
|
||
|
this.bind('change:table_name', function() {
|
||
|
this.table.set('id', this.get('table_name')).fetch();
|
||
|
}, this);
|
||
|
|
||
|
// schema changes should affect first to infowindow, tooltip
|
||
|
// and legend before table
|
||
|
this.bindInfowindow(this.infowindow, 'infowindow');
|
||
|
this.bindInfowindow(this.tooltip, 'tooltip');
|
||
|
this.bindLegend(this.legend);
|
||
|
this.bindTable(this.table);
|
||
|
|
||
|
this.tooltip.bind('change:fields', this._manageInteractivity, this);
|
||
|
|
||
|
if (this.get('table')) {
|
||
|
table_attr = this.get('table');
|
||
|
delete this.attributes.table;
|
||
|
this.table.set(table_attr);
|
||
|
}
|
||
|
if (!this.isTableLoaded()) {
|
||
|
this.table.fetch();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
isTableLoaded: function() {
|
||
|
return this.table.get('id') && this.table.get('privacy');
|
||
|
},
|
||
|
|
||
|
toLayerGroup: function() {
|
||
|
var attr = _.clone(this.attributes);
|
||
|
attr.layer_definition = {
|
||
|
version: '1.0.1',
|
||
|
layers: []
|
||
|
};
|
||
|
if (this.get('visible')) {
|
||
|
attr.layer_definition.layers.push(this.getLayerDef());
|
||
|
}
|
||
|
attr.type = 'layergroup'
|
||
|
return attr;
|
||
|
},
|
||
|
|
||
|
getLayerDef: function() {
|
||
|
var attr = this.attributes;
|
||
|
var query = attr.query || 'select * from ' + cdb.Utils.safeTableNameQuoting(attr.table_name);
|
||
|
if(attr.query_wrapper) {
|
||
|
query = _.template(attr.query_wrapper)({ sql: query });
|
||
|
}
|
||
|
return {
|
||
|
type: 'cartodb',
|
||
|
options: {
|
||
|
sql: query,
|
||
|
cartocss: this.get('tile_style'),
|
||
|
cartocss_version: '2.1.1',
|
||
|
interactivity: this.get('interactivity')
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Initializes the history if it doesn't exits and sets the position pointer to 0
|
||
|
* @param {string} property
|
||
|
*/
|
||
|
initHistory: function(property) {
|
||
|
if(!this.get(property+'_history')) {
|
||
|
this.set(property+'_history', []);
|
||
|
}
|
||
|
this[property+'_history_position'] = 0;
|
||
|
this[property+'_storage'] = new cdb.admin.localStorage(property+'_storage_'+this.get('table_name'));
|
||
|
},
|
||
|
/**
|
||
|
* Add a value to the property history if it's not the same than the last one
|
||
|
* @param {string} property
|
||
|
* @param {string} value
|
||
|
*/
|
||
|
addToHistory: function(property, value) {
|
||
|
if(value != this.get(property+'_history')[this.get(property+'_history').length - 1]) {
|
||
|
this.get(property+'_history').push(value);
|
||
|
this.trimHistory(property);
|
||
|
this[property+'_history_position'] = this.get(property+'_history').length - 1;
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Trim the history array to make sure its length isn't over MAX_HISTORY
|
||
|
* @param {String} property [description]
|
||
|
*/
|
||
|
trimHistory: function(property) {
|
||
|
var limit = this['MAX_HISTORY_'+property.toUpperCase()] ?
|
||
|
this['MAX_HISTORY_'+property.toUpperCase()] :
|
||
|
this.MAX_HISTORY;
|
||
|
while(this.get(property+'_history').length > limit) {
|
||
|
var removedValue = this.get(property+'_history').splice(0,1);
|
||
|
this[property+'_storage'].add(removedValue[0]);
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Moves the history position pointer n positions and notify that the property needs to be refreshed on views
|
||
|
* @param {String} property
|
||
|
* @param {Integer} n
|
||
|
*/
|
||
|
moveHistoryPosition: function(property, n) {
|
||
|
var newPosition = this[property+'_history_position'] + n;
|
||
|
if(newPosition >= 0 && newPosition < this.get(property+'_history').length) {
|
||
|
this[property+'_history_position'] = newPosition;
|
||
|
} else {
|
||
|
if(newPosition < 0 && Math.abs(newPosition) <= this[property+'_storage'].get().length) {
|
||
|
this[property+'_history_position'] = newPosition;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* returns the value saved on the position of the current _history_position, either from memory of from localStorage
|
||
|
* @param {String} property
|
||
|
* @return {String}
|
||
|
*/
|
||
|
getCurrentHistoryPosition: function(property) {
|
||
|
var currentPosition = this[property+'_history_position'];
|
||
|
if(this[property+'_history_position'] >= 0) {
|
||
|
return this.get(property+'_history')[currentPosition];
|
||
|
} else {
|
||
|
if(Math.abs(currentPosition) <= this[property+'_storage'].get().length) {
|
||
|
|
||
|
return this[property+'_storage'].get(this[property+'_storage'].get().length - Math.abs(currentPosition));
|
||
|
} else {
|
||
|
return this.get(property+'_history')[0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
},
|
||
|
/**
|
||
|
* Advances by one the history_position and returns the value saved on that pos
|
||
|
* @param {String} property
|
||
|
* @return {String}
|
||
|
*/
|
||
|
redoHistory: function(property) {
|
||
|
this.moveHistoryPosition(property, 1);
|
||
|
return this.getCurrentHistoryPosition(property);
|
||
|
},
|
||
|
/**
|
||
|
* Reduces by one the history_position and returns the value saved on that pos
|
||
|
* @param {String} property
|
||
|
* @return {String}
|
||
|
*/
|
||
|
undoHistory: function(property) {
|
||
|
var h = this.getCurrentHistoryPosition(property);
|
||
|
this.moveHistoryPosition(property, -1);
|
||
|
return h;
|
||
|
},
|
||
|
|
||
|
isHistoryAtLastPosition: function(property) {
|
||
|
if(this.get(property+'_history').length === 0) {
|
||
|
return true;
|
||
|
}
|
||
|
return ((this.get(property+'_history').length-1) == this[property+'_history_position']);
|
||
|
},
|
||
|
|
||
|
isHistoryAtFirstPosition: function(property) {
|
||
|
if(this.get(property+'_history').length === 0) {
|
||
|
return true;
|
||
|
}
|
||
|
var stored = this[property+'_storage'].get();
|
||
|
if(stored && stored.length === 0) {
|
||
|
if(this[property+'_history_position'] === 0) {
|
||
|
return true;
|
||
|
}
|
||
|
} else {
|
||
|
var storedSize = stored ? 1*stored.length : 0;
|
||
|
var minimumPos = -1* storedSize;
|
||
|
return (minimumPos == this[property+'_history_position']);
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
clone: function() {
|
||
|
var attr = _.clone(this.attributes);
|
||
|
delete attr.id;
|
||
|
attr.table = this.table.toJSON();
|
||
|
return new cdb.admin.CartoDBLayer(attr);
|
||
|
},
|
||
|
|
||
|
toJSON: function() {
|
||
|
var c = _.clone(this.attributes);
|
||
|
// remove api key
|
||
|
if(c.extra_params) {
|
||
|
c.extra_params = _.clone(this.attributes.extra_params);
|
||
|
if(c.extra_params.api_key) {
|
||
|
delete c.extra_params.api_key;
|
||
|
}
|
||
|
if(c.extra_params.map_key) {
|
||
|
delete c.extra_params.map_key;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
delete c.infowindow;
|
||
|
delete c.tooltip;
|
||
|
c.wizard_properties = this.wizard_properties.toJSON();
|
||
|
c.legend = this.legend.toJSON();
|
||
|
var d = {
|
||
|
// for some reason backend does not accept cartodb as layer type
|
||
|
kind: c.type.toLowerCase() === 'cartodb' ? 'carto': c.type,
|
||
|
options: c,
|
||
|
order: c.order,
|
||
|
infowindow: null,
|
||
|
tooltip: this.tooltip.toJSON()
|
||
|
};
|
||
|
|
||
|
// Don't send infowindow data if wizard doesn't support it
|
||
|
// It will make the tiler fails
|
||
|
if (this.wizard_properties.supportsInteractivity()) {
|
||
|
d.infowindow = this.infowindow.toJSON();
|
||
|
}
|
||
|
|
||
|
if(c.id !== undefined) {
|
||
|
d.id = c.id;
|
||
|
}
|
||
|
return d;
|
||
|
},
|
||
|
|
||
|
parse: function(data, xhr) {
|
||
|
var c = {};
|
||
|
// in case this is a response of saving the layer discard
|
||
|
// values from the server
|
||
|
if (!data || this._saving && !this.isNew()) {
|
||
|
return c;
|
||
|
}
|
||
|
|
||
|
// if api_key exist alread, set in params to not lose it
|
||
|
if(data.options.extra_params && this.attributes && this.attributes.extra_params) {
|
||
|
data.options.extra_params.map_key = this.attributes.extra_params.map_key;
|
||
|
}
|
||
|
|
||
|
var attrs = this.attributes;
|
||
|
var wp = attrs && attrs.wizard_properties;
|
||
|
|
||
|
if(wp && wp.properties && wp.properties.metadata) {
|
||
|
if (data.options.wizard_properties && data.options.wizard_properties.properties) {
|
||
|
data.options.wizard_properties.properties.metadata = wp.properties.metadata;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.wizard_properties && data.options.wizard_properties) {
|
||
|
this.wizard_properties.properties(data.options.wizard_properties);
|
||
|
}
|
||
|
|
||
|
_.extend(c, data.options, {
|
||
|
id: data.id,
|
||
|
type: data.kind === 'carto' ? 'CartoDB': data.kind,
|
||
|
infowindow: data.infowindow,
|
||
|
tooltip: data.tooltip,
|
||
|
order: data.order
|
||
|
});
|
||
|
|
||
|
return c;
|
||
|
},
|
||
|
|
||
|
sync: function(method, model, options) {
|
||
|
if(method != 'read') {
|
||
|
options.data = JSON.stringify(model.toJSON());
|
||
|
}
|
||
|
options.contentType = 'application/json';
|
||
|
options.url = model.url();
|
||
|
return Backbone.syncAbort(method, this, options);
|
||
|
},
|
||
|
|
||
|
unbindSQLView: function(sqlView) {
|
||
|
this.sqlView.unbind(null, null, this);
|
||
|
this.sqlView = null;
|
||
|
},
|
||
|
|
||
|
getCurrentState: function() {
|
||
|
if (this.error) {
|
||
|
return "error";
|
||
|
}
|
||
|
return "success";
|
||
|
},
|
||
|
|
||
|
bindSQLView: function(sqlView) {
|
||
|
var self = this;
|
||
|
this.sqlView = sqlView;
|
||
|
this.sqlView.bind('error', this.errorSQLView, this);
|
||
|
|
||
|
// on success and no modify rows save the query!
|
||
|
this.sqlView.bind('reset', function() {
|
||
|
// if the query was cleared while fetchin the data
|
||
|
if (!self.table.isInSQLView()) return;
|
||
|
self.error = false;
|
||
|
if(self.sqlView.modify_rows) {
|
||
|
self.set({ query: null });
|
||
|
self.invalidate();
|
||
|
self.table.useSQLView(null, { force_data_fetch: true });
|
||
|
self.trigger('clearSQLView');
|
||
|
} else {
|
||
|
self.save({
|
||
|
query: sqlView.getSQL(),
|
||
|
sql_source: sqlView.sqlSource()
|
||
|
});
|
||
|
}
|
||
|
}, this);
|
||
|
|
||
|
// Set sql view if query was applied
|
||
|
var sql = this.get('query');
|
||
|
if (sql) {
|
||
|
this.applySQLView(sql, { add_to_history: false });
|
||
|
} else {
|
||
|
this.table.data().fetch();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
bindTable: function(table) {
|
||
|
this.table = table;
|
||
|
var self = this;
|
||
|
self.table.bind('change:name', function() {
|
||
|
if (self.get('table_name') != self.table.get('name')) {
|
||
|
self.fetch({
|
||
|
success: function() {
|
||
|
self.updateCartoCss(self.table.previous('name'), self.table.get('name'));
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.table.bind('change:schema', this._manageInteractivity, this);
|
||
|
},
|
||
|
|
||
|
_manageInteractivity: function() {
|
||
|
var interactivity = null;
|
||
|
if (this.wizard_properties.supportsInteractivity()) {
|
||
|
if(this.table.containsColumn('cartodb_id')) {
|
||
|
interactivity = ['cartodb_id'];
|
||
|
}
|
||
|
var tooltipColumns = this.tooltip.getInteractivity();
|
||
|
if (tooltipColumns.length) {
|
||
|
interactivity = (interactivity || []).concat(tooltipColumns);
|
||
|
}
|
||
|
if (interactivity) {
|
||
|
interactivity = interactivity.join(',');
|
||
|
}
|
||
|
}
|
||
|
if(this.get('interactivity') !== interactivity) {
|
||
|
if (this.isNew()) {
|
||
|
this.set({ interactivity: interactivity });
|
||
|
} else {
|
||
|
this.save({ interactivity: interactivity });
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates the style chaging the table name for a new one
|
||
|
* @param {String} previousName
|
||
|
* @param {String} newName
|
||
|
*/
|
||
|
updateCartoCss: function(previousName, newName) {
|
||
|
var tileStyle = this.get('tile_style');
|
||
|
if (!tileStyle) return;
|
||
|
var replaceRegexp = new RegExp('#'+previousName, 'g');
|
||
|
tileStyle = tileStyle.replace(replaceRegexp, '#'+newName);
|
||
|
this.save({'tile_style': tileStyle});
|
||
|
},
|
||
|
|
||
|
bindLegend: function(legend) {
|
||
|
|
||
|
var data = this.get('legend');
|
||
|
|
||
|
if (data) {
|
||
|
this.legend.set(data);
|
||
|
}
|
||
|
|
||
|
this.legend.bind('change:template change:type change:title change:show_title change:items', _.debounce(function() {
|
||
|
// call with silent so the layer is no realoded
|
||
|
// if some view needs to be changed when the legend is changed it should be
|
||
|
// subscribed to the legend model not to dataLayer
|
||
|
// (which is only a data container for the legend)
|
||
|
if (!this.isNew()) {
|
||
|
this.save(null, { silent: true });
|
||
|
}
|
||
|
}, 250), this);
|
||
|
|
||
|
},
|
||
|
|
||
|
bindInfowindow: function(infowindow, attr) {
|
||
|
attr = attr || 'infowindow';
|
||
|
var infowindowData = this.get(attr);
|
||
|
if(infowindowData) {
|
||
|
infowindow.set(infowindowData);
|
||
|
} else {
|
||
|
// assign a position from start
|
||
|
var pos = 0;
|
||
|
_(this.table.get('schema')).each(function(v) {
|
||
|
if(!_.contains(['the_geom', 'created_at', 'updated_at', 'cartodb_id'], v[0])) {
|
||
|
infowindow.addField(v[0], pos);
|
||
|
++pos;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this.table.linkToInfowindow(infowindow);
|
||
|
|
||
|
var watchedFields = 'change:fields change:template_name change:alternative_names change:template change:disabled change:width change:maxHeight';
|
||
|
var deferredSave = _.debounce(function() {
|
||
|
// call with silent so the layer is no realoded
|
||
|
// if some view needs to be changed when infowindow is changed it should be
|
||
|
// subscribed to infowindow model not to dataLayer
|
||
|
// (which is only a data container for infowindow)
|
||
|
|
||
|
// since removeMissingFields changes fields, unbind changes temporally
|
||
|
infowindow.unbind(watchedFields, deferredSave, this);
|
||
|
// assert all the fields are where they should
|
||
|
infowindow.removeMissingFields(this.table.columnNames());
|
||
|
infowindow.bind(watchedFields, deferredSave, this);
|
||
|
if (!this.isNew()) {
|
||
|
this.save(null, { silent: true });
|
||
|
}
|
||
|
}, 250);
|
||
|
|
||
|
infowindow.bind(watchedFields, deferredSave, this);
|
||
|
},
|
||
|
|
||
|
resetQuery: function() {
|
||
|
if (this.get('query')) {
|
||
|
this.save({
|
||
|
query: undefined,
|
||
|
sql_source: null
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
errorSQLView: function(m, e) {
|
||
|
this.save({ query: null }, { silent: true });
|
||
|
this.trigger('errorSQLView', e);
|
||
|
this.error = true;
|
||
|
},
|
||
|
|
||
|
clearSQLView: function() {
|
||
|
// before reset query we should remove the view
|
||
|
this.table.useSQLView(null);
|
||
|
this.addToHistory("query", 'SELECT * FROM ' + cdb.Utils.safeTableNameQuoting(this.table.get('name')));
|
||
|
// undo history to move backwards the history pointer
|
||
|
this.undoHistory("query");
|
||
|
this.resetQuery();
|
||
|
this.trigger('clearSQLView');
|
||
|
},
|
||
|
|
||
|
applySQLView: function(sql, options) {
|
||
|
options = options || {
|
||
|
add_to_history: true,
|
||
|
sql_source: null
|
||
|
};
|
||
|
// if the sql change the table data do not save in the data layer
|
||
|
// pass though and launch the query directly to the table
|
||
|
this.table.useSQLView(this.sqlView);
|
||
|
this.sqlView.setSQL(sql, {
|
||
|
silent: true,
|
||
|
sql_source: options.sql_source || null
|
||
|
});
|
||
|
if(options.add_to_history) {
|
||
|
this.addToHistory("query", sql);
|
||
|
}
|
||
|
// if there is some error the query is rollbacked
|
||
|
this.sqlView.fetch();
|
||
|
this.trigger('applySQLView', sql);
|
||
|
},
|
||
|
|
||
|
moveToFront: function(opts) {
|
||
|
var layers = this.collection;
|
||
|
var dataLayers = layers.getDataLayers();
|
||
|
|
||
|
layers.moveLayer(this, { to: dataLayers.length });
|
||
|
}
|
||
|
}, {
|
||
|
|
||
|
createDefaultLayerForTable: function(tableName, userName) {
|
||
|
return new cdb.admin.CartoDBLayer({
|
||
|
user_name: userName,
|
||
|
table_name: tableName,
|
||
|
tile_style: "#" + tableName + cdb.admin.CartoStyles.DEFAULT_GEOMETRY_STYLE,
|
||
|
style_version: '2.1.0',
|
||
|
visible: true,
|
||
|
interactivity: 'cartodb_id',
|
||
|
maps_api_template: cdb.config.get('maps_api_template'),
|
||
|
no_cdn: true
|
||
|
});
|
||
|
}
|
||
|
|
||
|
});
|