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 }); } });