cartodb/lib/assets/javascripts/builder/data/analysis-definition-node-model.js
2020-06-15 10:58:47 +08:00

328 lines
9.4 KiB
JavaScript
Executable File

var _ = require('underscore');
var Backbone = require('backbone');
var camshaftReference = require('./camshaft-reference');
var QueryGeometryModel = require('./query-geometry-model');
var QuerySchemaModel = require('./query-schema-model');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var QueryRowsCollection = require('./query-rows-collection');
var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects');
var layerColors = require('builder/data/layer-colors');
var REQUIRED_OPTS = [
'configModel'
];
/**
* Base model for an analysis definition node.
* May point to one or multiple nodes in turn (referenced by ids).
*
*
* Analysis-definition-node-model
* ├── Query-Schema-Model
* ├── Query-Geometry-Model
* └── Query-Rows-Collection
*/
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!this.id) throw new Error('id is required');
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
var modelOpts = {
configModel: this._configModel,
userModel: opts.userModel
};
var simpleGeom = this.get('simple_geom');
this.querySchemaModel = new QuerySchemaModel(undefined, modelOpts);
this.queryGeometryModel = new QueryGeometryModel({
simple_geom: simpleGeom,
status: simpleGeom ? 'fetched' : 'unfetched'
}, modelOpts);
this.queryRowsCollection = new QueryRowsCollection([], {
configModel: this._configModel,
userModel: opts.userModel,
tableName: '',
querySchemaModel: this.querySchemaModel
});
this.listenTo(this.queryGeometryModel, 'change:simple_geom', this._onGeometryTypeChanged);
},
fetchQueryObjects: function () {
if (this.get('status') === 'ready' && !this.get('error')) {
fetchAllQueryObjects({
queryGeometryModel: this.queryGeometryModel,
querySchemaModel: this.querySchemaModel,
queryRowsCollection: this.queryRowsCollection
});
}
},
/**
* @override {Backbone.prototype.parse} flatten the provided analysis data and create source nodes if there are any.
*/
parse: function (r, opts) {
// Allow omitting type when merging into an existing node
var sourceNames = (!r.type && opts.merge) ? [] : camshaftReference.getSourceNamesForAnalysisType(r.type);
var parsedParams = _.reduce(r.params, function (memo, val, name) {
var sourceName = sourceNames[sourceNames.indexOf(name)];
if (sourceName) {
this.collection.add(val, opts);
memo[name] = val.id;
} else {
memo[name] = val;
}
return memo;
}, {}, this);
var parsedOptions = _.reduce(r.options, function (memo, val, name) {
memo[name] = val;
return memo;
}, {}, this);
return _.defaults(
_.omit(r, 'params', 'options'),
parsedOptions,
parsedParams
);
},
/**
* @override {Backbone.prototype.toJSON} unflatten the internal structure to the expected nested JSON data structure.
* @param {Object} options
* @param {Boolean} options.skipOptions - Add or not analysis options to the definition
*/
toJSON: function (options) {
options = options || {};
var analysisParams = [];
var paramsforJSON = {};
var paramsForType = camshaftReference.paramsForType(this.get('type'));
// Undo the parsing of the params previously done in .parse() (when model was created)
for (var name in paramsForType) {
var param = paramsForType[name];
var val = this.get(name);
var includeParamInJSON = true;
if (param.type === 'node') {
var node = this.collection.get(val);
if (node) {
val = node.toJSON(options);
} else if (param.optional) { // Node has no value but is optional. We don't serialize it.
includeParamInJSON = false;
} else { // Node has no value and is required. Throw error.
throw new Error('no node found for param "' + name + '" with id "' + val + '" in node ' + JSON.stringify(this.attributes));
}
}
if (includeParamInJSON) {
paramsforJSON[name] = val;
analysisParams.push(name);
}
}
var json = {
id: this.get('id'),
type: this.get('type'),
params: paramsforJSON
};
// style_history is not sent at all (neither inside params nor options). It is always set by the backend.
var optionsAttrs = _.omit(this.attributes, analysisParams.concat('id', 'type', 'status', 'style_history'));
if (!(options.skipOptions || _.isEmpty(optionsAttrs))) {
json['options'] = optionsAttrs;
}
return json;
},
/**
* @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
*/
isNew: function () {
return true;
},
isReadOnly: function () {
// By default, any node should be read only by default, except the source node (with exceptions)
return true;
},
isCustomQueryApplied: function () {
// By default, any node shouldn't have a custom query applied, except the source node
return false;
},
canBeDeletedByUser: function () {
return this.hasPrimarySource();
},
hasFailed: function () {
return this.get('status') === 'failed';
},
isDone: function () {
return this.get('status') === 'ready';
},
destroy: function () {
if (this.querySchemaModel) {
this.querySchemaModel.destroy();
this.querySchemaModel = null;
}
return Backbone.Model.prototype.destroy.apply(this, arguments);
},
/**
* Creates a new node that have same params as the current node, with only the id differing.
* @param {String} newId - e.g. 'b1'
* @return {Object} instance of analysis-definition-node-model
*/
clone: function (newId) {
if (!newId) throw new Error('newId is required');
if (!_.isString(newId) || newId.length < 2) throw new Error('newId is required as a non-empty string');
if (newId === this.id) throw new Error('newId must be different from current id, ' + this.id);
var attrs = this.toJSON();
attrs.id = newId;
return this.collection.add(attrs);
},
linkedListBySameLetter: function () {
var list = [this];
var prev = this;
var letter = this.letter();
while (true) {
var current = prev.getPrimarySource();
if (current && current.letter() === letter) {
list.push(current);
prev = current;
} else {
break;
}
}
return list;
},
/**
* @return {Boolean, null} null if the geoemtry type of this node is not known,
* this is to indicate that the query model is not ready yet, up to caller to make the call or not
*/
isValidAsInputForType: function (analysisType) {
if (analysisType === 'source') return false;
var geometry = this.queryGeometryModel.get('simple_geom');
return geometry
? camshaftReference.isValidInputGeometryForType(geometry, analysisType)
: null;
},
containsNode: function (other) {
if (!other) return false;
return this.id === other.id || this._sourcesContains(other);
},
hasPrimarySource: function () {
return !!this.getPrimarySource();
},
hasSecondarySource: function () {
return !!this.getSecondarySource();
},
getPrimarySource: function () {
return this.collection.get(this._getPrimarySourceId());
},
getSecondarySource: function () {
// Assumption: there are only 1-2 sources, so secondary is the one that's not the primary
var primarySourceId = this._getPrimarySourceId();
var secondarySourceId = _.find(this.sourceIds(), function (sourceId) {
return sourceId !== primarySourceId;
});
return this.collection.get(secondarySourceId);
},
// Default query by default is the query already applied in the node.
// For source node is totally different
getDefaultQuery: function () {
return this.get('query');
},
/**
* @return {Array} e.g. ['c3', 'b2']
*/
sourceIds: function () {
return _.map(this._sourceNames(), function (sourceName) {
return this.get(sourceName);
}, this);
},
changeSourceIds: function (currentId, newId, silenty) {
this._sourceNames().forEach(function (sourceName) {
if (this.get(sourceName) === currentId) {
this.set(sourceName, newId, { silent: silenty });
}
}, this);
},
isSourceType: function () {
return this.get('type') === 'source';
},
letter: function () {
return nodeIds.letter(this.id);
},
getColor: function () {
var letter = nodeIds.letter(this.id);
return layerColors.getColorForLetter(letter);
},
getStyleHistoryForLayer: function (layer_id) {
var styleHistory = this.get('style_history');
return styleHistory && styleHistory[layer_id];
},
// Private methods
_sourcesContains: function (other) {
var primarySource = this.getPrimarySource();
var secondarySource = this.getSecondarySource();
return !!(
(primarySource && primarySource.containsNode(other)) ||
(secondarySource && secondarySource.containsNode(other))
);
},
_getPrimarySourceId: function () {
var primarySourceName = this.get('primary_source_name') || this._sourceNames()[0];
return this.get(primarySourceName);
},
/**
* @return {Array} e.g. ['polygons_source', 'points_source']
*/
_sourceNames: function () {
return camshaftReference.getSourceNamesForAnalysisType(this.get('type'));
},
_onGeometryTypeChanged: function (model, value) {
this.set('simple_geom', value);
}
});