cartodb-4.42/lib/assets/javascripts/cartodb/models/cartodb_layer.js

526 lines
16 KiB
JavaScript
Raw Normal View History

2024-04-06 13:25:13 +08:00
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
});
}
});