// form validation var alwaysTrueValidator = function(form) { return true }; function columnExistsValidatorFor(column_name) { return function(form) { var field = form[column_name]; return field.form.property.extra.length > 0; }; } var columnExistsValidator = columnExistsValidatorFor('Column'); // // defines a form schema, what fields contains and so on // cdb.admin.FormSchema = cdb.core.Model.extend({ validators: { polygon: alwaysTrueValidator, cluster: alwaysTrueValidator, intensity: alwaysTrueValidator, bubble: columnExistsValidator, choropleth: columnExistsValidator, color: columnExistsValidator, category: columnExistsValidator, density: alwaysTrueValidator, torque: columnExistsValidatorFor('Time Column'), torque_cat: columnExistsValidatorFor('Time Column'), torque_heat: columnExistsValidatorFor('Time Column') }, initialize: function() { this.table = this.get('table'); this.unset('table'); if(!this.table) throw new Error('table is undefined'); // validate type // it should be polygon, bubble or some of the defined wizard types var type = this.get('type'); if(!type) { throw new Error('type is undefined'); } // get the default values var form_data = this.defaultFor(type); if (!form_data) { throw new Error('invalid type: ' + type); } // assign index to be able to compose the order form_data.forEach(function(v, i) { v.index = i }); this.set(_.object(_.pluck(form_data, 'name'), form_data), { silent: true }); this._fillColumns(); this.table.bind('change:schema', function() { var opts = {}; if (!this.table.previous('schema')) { opts.silent = true; } this._fillColumns(opts); if (opts.silent) { this._previousAttributes = _.clone(this.attributes); } }, this); }, toJSON: function() { var form_data = _.values(_.omit(this.attributes, 'type')); form_data.sort(function(a, b) { return a.index - b.index; }); return form_data; }, _fillColumns: function(opts) { var self = this; // lazy shallow copy var attrs = JSON.parse(JSON.stringify(this.attributes)); _.each(attrs, function(field) { for (var k in field.form) { var f = field.form[k]; if (f.columns) { var types = f.columns.split('|'); var extra = []; if (f.extra_default) extra = f.extra_default.slice(); for(var i in types) { var type = types[i]; var columns = self.table.columnNamesByType(type); extra = extra.concat( _.without(columns, 'cartodb_id') ) if (f.default_column === type) { var customColumns = _.without(columns, 'cartodb_id', 'created_at', 'updated_at'); if (customColumns.length) { f.value = customColumns[0]; } } } if (!f.value) f.value = extra[0]; else if (!_.contains(extra, f.value)) { f.value = extra[0]; } f.extra = extra; } } }); this.set(attrs, opts); }, defaultFor: function(type) { var form_data = cdb.admin.forms.get(type)[this.table.geomColumnTypes()[0] || 'point']; return form_data; }, // return the default style properties // based on forms value style: function(props) { var default_data = {}; _(this.attributes).each(function(field) { if (props && !_.contains(props, field)) return; _(field.form).each(function(v, k) { default_data[k] = v.value; }); }); return default_data; }, isValid: function(type) { return this.validators[type || 'polygon'](this.attributes); }, // return true if this form was valid before the current change // this method should be only called during a change event wasValid: function(type) { return this.validators[type](this.previousAttributes()); }, dynamicProperties: function() { var props = []; _.each(this.attributes, function(field) { for (var k in field.form) { var f = field.form[k]; if (f.columns) { props.push(field); } } }); return props; }, // return true is some property used to regenerate style has been changed changedDinamycProperty: function() { var changed = []; var d = this.dynamicProperties(); for(var i in d) { if (this.changedAttributes(d[i])) { changed.push(d[i]); } } return changed; }, dinamycProperty: function(c) { return _.keys(this.get(c.name).form)[0]; }, dinamycValues: function(c) { var v = this.get(c.name); var k = this.dinamycProperty(c); return v.form[k].extra; } }); cdb.admin.WizardProperties = cdb.core.Model.extend({ initialize: function() { // params this.table = this.get('table'); this.unset('table'); if(!this.table) throw new Error('table is undefined'); this.layer = this.get('layer'); this.unset('layer'); if(!this.layer) throw new Error('layer is undefined'); // stores forms for geometrys and type this.forms = {}; this._savedStates = {}; this.cartoStylesGeneration = new cdb.admin.CartoStyles(_.extend({}, this.layer.get('wizard_properties'), { table: this.table }) ); if (this.attributes.properties && _.keys(this.attributes.properties).length !== 0) { this.properties(this.attributes); } delete this.attributes.properties; // bind loading and load this.cartoStylesGeneration.bind('load', function() { this.trigger('load'); }, this) this.cartoStylesGeneration.bind('loading', function() { this.trigger('loading'); }, this) this.table.bind('columnRename', function(newName, oldName) { if (this.isDisabled()) return; var attrs = {}; // search for columns for(var k in this.attributes) { if(this.get(k) === oldName) { attrs[k] = newName; } } this.set(attrs); }, this); // when table schema changes regenerate styles // notice this not update properties, only regenerate // the style this.table.bind('change:schema', function() { if (!this.isDisabled() && this.table.previous('schema') !== undefined) this.cartoStylesGeneration.regenerate(); }, this); this.table.bind('change:geometry_types', function() { if(!this.table.changedAttributes()) { return; } var geoTypeChanged = this.table.geometryTypeChanged(); if(geoTypeChanged) this.trigger('change:form'); var prev = this.table.previous('geometry_types'); var current = this.table.geomColumnTypes(); // wizard non initialized if((!prev || prev.length === 0) && !this.get('type')) { this.active('polygon'); return; } if (!current || current.length === 0) { if (!this.table.isInSQLView()) { // empty table this.unset('type', { silent: true }); } return; } if (!prev || prev.length === 0) return; if (geoTypeChanged) { this.active('polygon', {}, { persist: false }); } }, this); this.linkLayer(this.layer); this.bindGenerator(); // unbind previous form and bind the new one this.bind('change:type', this._updateForm); this.table.bind('change:geometry_types', this._updateForm, this); this._updateForm(); // generator should be always filled in case sql // or table schema is changed this._fillGenerator({ silent: true }); }, _updateForm: function() { //unbind all forms for(var k in this.forms) { var forms = this.forms[k]; for(var f in forms) { var form = forms[f]; form.unbind(null, null, this); } } var t = this.get('type'); if (t) { var f = this._form(t); f.bind('change', function() { if (!f.isValid(this.get('type'))) { this.active('polygon'); } else if(!f.wasValid(this.get('type'))) { if(!this.isDisabled()) { // when the form had no column previously // that means the wizard was invalid this.active(this.get('type'), null, { persist: false, restore: false }); } } else { var self = this; var c = f.changedDinamycProperty(); var propertiesChanged = []; if(c.length) { _.each(c, function(form_p) { var k = f.dinamycProperty(form_p); if (self.has(k) && !_.contains(f.dinamycValues(form_p), self.get(k))) { propertiesChanged.push(form_p); } }); if (propertiesChanged.length) { var st = f.style(propertiesChanged); this.set(st); } } } this.trigger('change:form'); }, this); } }, _form: function(type, geomType) { var form = this.forms[type] || (this.forms[type] = {}); geomType = geomType || this.table.geomColumnTypes()[0] || 'point'; if (!form[geomType]) { form[geomType] = new cdb.admin.FormSchema({ table: this.table, type: type || 'polygon' }); form[geomType].__geomType = geomType; } return form[geomType]; }, formData: function(type) { var self = this; var form = this._form(type); return form.toJSON(); }, defaultStyleForType: function(type) { return this._form(type).style(); }, // save current state saveCurrent: function(type, geom) { var k = type + "_" + geom; this._savedStates[k] = _.clone(this.attributes); }, getSaved: function(type, geom) { var k = type + "_" + geom; return this._savedStates[k] || {}; }, // active a wizard type active: function(type, props, opts) { opts = _.defaults(opts || {}, { persist: true }); // if the geometry is undefined the wizard can't be applied var currentGeom = this.table.geomColumnTypes()[0]; if (!currentGeom) { return; } opts = _.defaults(opts || {}, { persist: true, restore: true }); // previously category map was called color. this avoids // color wizard is enabled since it's compatible with category if (type === "color") type = 'category'; // if the geometry type has changed do not allow to persist previous // properties. This avoids cartocss properties from different // geometries are mixed if (this.get('geometry_type') && currentGeom !== this.get('geometry_type')) { opts.persist = false; } // get the default props for current type and use previously saved // attributes to override them var geomForm = this.defaultStyleForType(type); var current = (opts.persist && type === this.get('type')) ? this.attributes: {}; _.extend(geomForm, opts.restore ? this.getSaved(type, currentGeom): {}, current, props); geomForm.type = type; geomForm.geometry_type = currentGeom; // if the geometry is invalid, do not save previous attributes var t = this.get('type'); var gt = this.get('geometry_type'); if(t && gt && this._form(t, gt).isValid(t)) { this.saveCurrent(t, gt); } this.clear({ silent: true }); this.cartoStylesGeneration.unset('metadata', {silent: true}); this.cartoStylesGeneration.unset('properties', { silent: true }); // set layer as enabled to change style this.enableGeneration(); this.set(geomForm); }, enableGeneration: function() { this.layer.set('tile_style_custom', false, { silent: true }); }, // the style generation can be disabled because of a custom style isDisabled: function() { return this.layer.get('tile_style_custom'); }, properties: function(props) { if (!props) return this; var t = props.type === 'color' ? 'category': props.type; var vars = _.extend( { type: t }, props.properties ); return this.set(vars); }, _fillGenerator: function(opts) { opts = opts || {} this.cartoStylesGeneration.set({ 'properties': _.clone(this.attributes), 'type': this.get('type') }, opts); }, _updateGenerator: function() { var t = this.get('type'); var isValid = this._form(t).isValid(t); this._fillGenerator({ silent: !isValid || this.isDisabled() }); }, bindGenerator: function() { // every time properties change update the generator this.bind('change', this._updateGenerator, this); }, unbindGenerator: function() { this.unbind('change', this._updateGenerator, this); }, toJSON: function() { return { type: this.get('type'), properties: _.omit(this.attributes, 'type', 'metadata') }; }, linkLayer: function(layer) { var self = this; /* * this is disabled because we need to improve propertiesFromStyle method * in order to not override properties which shouldn't be, see CDB-1566 * layer.bind('change:tile_style', function() { if(this.isDisabled()) { this.unbindGenerator(); this.set(this.propertiesFromStyle(layer.get('tile_style'))); this.bindGenerator(); } }, this); */ layer.bind('change:query', function() { if(!this.isDisabled()) this.cartoStylesGeneration.regenerate(); }, this); var changeLayerStyle = function(st, sql, layerType) { layerType = layerType || 'CartoDB'; // update metadata from cartocss generation self.unbindGenerator(); var meta = self.cartoStylesGeneration.get('metadata'); if (meta) { self.set('metadata', meta); } else { self.unset('metadata'); } self.bindGenerator(); var attrs = { tile_style: st, type: layerType, tile_style_custom: false }; if(sql) { attrs.query_wrapper = sql.replace(/__wrapped/g, '(<%= sql %>)');//"with __wrapped as (<%= sql %>) " + sql; } else { attrs.query_wrapper = null; } attrs.query_generated = attrs.query_wrapper !== null; // update the layer model if (layer.isNew() || !layer.collection) { layer.set(attrs); } else { layer.save(attrs); } }; // this is the sole entry point where the cartocss is changed. this.cartoStylesGeneration.bind('change:style change:sql', function() { var st = this.cartoStylesGeneration.get('style'); if(st) { changeLayerStyle( st, this.cartoStylesGeneration.get('sql'), this.get('layer-type') ); } }, this); }, unlinkLayer: function(layer) { this.unbind(null, null, layer); layer.unbind(null, null, this); }, getEnabledWizards: function() { var _enableMap = { 'point': ['polygon', 'cluster', 'choropleth', 'bubble', 'density', 'category', 'intensity', 'torque', 'torque_cat', 'torque_heat'], 'line':['polygon', 'choropleth', 'category', 'bubble'], 'polygon': ['polygon', 'choropleth', 'category', 'bubble'] }; return _enableMap[this.table.geomColumnTypes()[0] || 'point']; }, //MOVE to the model propertiesFromStyle: function(cartocss) { var parser = new cdb.admin.CartoParser(); var parsed = parser.parse(cartocss); if (!parsed) return {}; var rules = parsed.getDefaultRules(); if(parser.errors().length) return {}; var props = {}; var t = this._getTypeFromCSS(cartocss); var valid_attrs =_.uniq(_.keys(this.attributes).concat(_.keys(this._form(t).style()))); if (rules) { for(var p in valid_attrs) { var prop = valid_attrs[p]; var rule = rules[prop]; if (rule) { rule = rule.ev(); if (!carto.tree.Reference.validValue(parser.parse_env, rule.name, rule.value)) { return {}; } var v = rule.value.ev(this.parse_env); if (v.is === 'color') { v = v.toString(); } else if (v.is === 'uri') { v = 'url(' + v.toString() + ')'; } else { v = v.value; } props[prop] = v; } } if("image-filters" in props && !props["image-filters"]){ props["image-filters"] = rules["image-filters"].value.value[0].value[0] } return props; } return {}; }, _getTypeFromCSS: function(css) { if (css.indexOf("colorize-alpha") > -1) { return "torque_heat"; } else if (css.indexOf("torque-time-attribute") > -1) { return "torque"; } else { return this.get('type'); } }, // returns true if current wizard supports user // interaction supportsInteractivity: function() { var t = this.get('type'); if (_.contains(['torque', 'cluster', 'density', 'torque_cat'], t)) { return false; } return true; } });