cartodb-4.42/lib/assets/javascripts/cartodb/models/wizard.js
2024-04-06 05:25:13 +00:00

580 lines
16 KiB
JavaScript

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