cartodb/lib/assets/javascripts/builder/data/layer-definition-model.js

490 lines
15 KiB
JavaScript
Raw Normal View History

2020-06-15 10:58:47 +08:00
var Backbone = require('backbone');
var _ = require('underscore');
var syncAbort = require('./backbone/sync-abort');
var StyleDefinitionModel = require('builder/editor/style/style-definition-model');
var StyleCartoCSSModel = require('builder/editor/style/style-cartocss-model');
var DataSQLModel = require('builder/editor/layers/layer-content-views/data/data-sql-model');
var layerTypesAndKinds = require('./layer-types-and-kinds');
var InfowindowModel = require('./infowindow-click-model');
var TooltipModel = require('./infowindow-hover-model');
var TableNameUtils = require('builder/helpers/table-name-utils');
var layerColors = require('./layer-colors');
// from_layer_id and from_letter are not attributes for the model, but are sent to the layer creation
// endpoint when creating a layer from an existing analysis node (see user-actions)
var ATTR_NAMES = ['id', 'order', 'infowindow', 'tooltip', 'error', 'from_layer_id', 'from_letter'];
/**
* Model to edit a layer definition.
* Should always exist as part of a LayerDefinitionsCollection, so its URL is given from there.
*/
module.exports = Backbone.Model.extend({
/**
* @override {Backbone.prototype.sync} abort ongoing request if there is any
*/
sync: syncAbort,
parse: function (response, opts) {
response.options = response.options || {};
// Flatten the attrs, to avoid having this.get('options').foobar internally
var attrs = _
.defaults(
_.pick(response, ATTR_NAMES),
_.omit(response.options, ['query', 'tile_style'])
);
// Only use type on the frontend, it will be mapped back when the model is serialized (see .toJSON)
attrs.type = attrs.type || layerTypesAndKinds.getType(response.kind);
// Map API endpoint attrs to the new names used client-side (cartodb.js in particular)
if (response.options.tile_style) {
attrs.cartocss = response.options.tile_style;
}
if (response.options.query) {
attrs.sql = response.options.query;
}
if (response.infowindow) {
if (!this.infowindowModel) {
this.infowindowModel = new InfowindowModel(response.infowindow, {
configModel: opts.configModel || this._configModel
});
}
}
if (response.tooltip) {
if (!this.tooltipModel) {
this.tooltipModel = new TooltipModel(response.tooltip, {
configModel: opts.configModel || this._configModel
});
}
}
if (response.options.table_name) {
// Set autostyle as false if it doesn't contain any id
attrs.autoStyle = attrs.autoStyle || false;
if (!this.styleModel) {
this.styleModel = new StyleDefinitionModel(response.options.style_properties, {
parse: true
});
this.cartocssModel = new StyleCartoCSSModel({
content: attrs.cartocss
}, {
history: response.options.cartocss_history || response.options.tile_style_history
});
}
if (!this.sqlModel) {
this.sqlModel = new DataSQLModel({
content: attrs.sql
}, {
history: response.options.sql_history
});
}
}
// Flatten the rest of the attributes
return attrs;
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this.on('change:source change:sql', this._onPosibleLayerSchemaChanged, this);
if (this.styleModel) {
this.styleModel.bind('change:type change:animated', function () {
if (this.styleModel.isAggregatedType() || this.styleModel.isAnimation()) {
// setTemplate will clear fields
this.infowindowModel && this.infowindowModel.unsetTemplate();
this.tooltipModel && this.tooltipModel.unsetTemplate();
}
}, this);
}
},
save: function (attrs, options) {
attrs = attrs || {};
options = options || {};
// We assume that if the layer is saved, we have to disable autostyle
var autoStyleAttrs = {
autoStyle: false
};
// But if the layer is saved with shouldPreserveAutoStyle option, we should preserve autostyle
if (options && options.shouldPreserveAutoStyle) {
delete autoStyleAttrs.autoStyle;
} else if (this.get('autoStyle')) {
this.styleModel && this.styleModel.resetPropertiesFromAutoStyle();
}
attrs = _.extend(
{},
autoStyleAttrs,
attrs
);
return Backbone.Model.prototype.save.call(this, attrs, options);
},
toJSON: function () {
// Un-flatten the internal attrs to the datastructure that's expected by the API endpoint
var options = _.omit(this.attributes, ATTR_NAMES.concat(['cartocss', 'sql', 'autoStyle']));
// Map back internal attrs to the expected attrs names by the API endpoint
var cartocss = this.get('cartocss');
if (cartocss) {
options.tile_style = cartocss;
}
var sql = this.get('sql');
if (sql) {
options.query = sql;
}
var defaultAttributes = {
kind: layerTypesAndKinds.getKind(this.get('type')),
options: options
};
var infowindowData = this.infowindowModel && this.infowindowModel.toJSON();
if (!_.isEmpty(infowindowData)) {
defaultAttributes.infowindow = this.infowindowModel.toJSON();
}
var tooltipData = this.tooltipModel && this.tooltipModel.toJSON();
if (!_.isEmpty(tooltipData)) {
defaultAttributes.tooltip = this.tooltipModel.toJSON();
}
if (this.styleModel && !this.styleModel.isAutogenerated()) {
defaultAttributes.options.style_properties = this.styleModel.toJSON();
}
if (this.cartocssModel) {
defaultAttributes.options.cartocss_history = this.cartocssModel.getHistory();
}
if (this.sqlModel) {
defaultAttributes.options.sql_history = this.sqlModel.getHistory();
}
var attributes = _.omit(this.attributes, 'infowindow', 'tooltip', 'options', 'error', 'autoStyle');
return _.defaults(
defaultAttributes,
_.pick(attributes, ATTR_NAMES)
);
},
canBeDeletedByUser: function () {
return this.collection.getNumberOfDataLayers() > 1 && this.isDataLayer() &&
(this._canBeFoldedUnderAnotherLayer() || !this._isAllDataLayersDependingOnAnyAnalysisOfThisLayer());
},
isOwnerOfAnalysisNode: function (nodeModel) {
return nodeModel && nodeModel.letter() === this.get('letter');
},
ownedPrimaryAnalysisNodes: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return this.isOwnerOfAnalysisNode(nodeDefModel)
? nodeDefModel.linkedListBySameLetter()
: [];
},
getName: function () {
return this.get('name') ||
this.get('table_name_alias') ||
this.get('table_name');
},
getTableName: function () {
return this.get('table_name') || '';
},
getColor: function () {
return layerColors.getColorForLetter(this.get('letter'));
},
containsNode: function (other) {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return nodeDefModel && nodeDefModel.containsNode(other);
},
getAnalysisDefinitionNodeModel: function () {
return this.findAnalysisDefinitionNodeModel(this.get('source'));
},
findAnalysisDefinitionNodeModel: function (id) {
return this.collection && this.collection.findAnalysisDefinitionNodeModel(id);
},
_onPosibleLayerSchemaChanged: function (eventName, attrs, options) {
// Used to avoid resetting styles on source_id changes when we have saved styles for the node
if (options && options.ignoreSchemaChange) {
return;
}
if (this.infowindowModel) {
this.infowindowModel.clearFields();
}
if (this.tooltipModel) {
this.tooltipModel.clearFields();
}
if (this.styleModel) {
this.styleModel.resetStyles();
}
},
toggleVisible: function () {
this.set('visible', !this.get('visible'));
},
toggleCollapse: function () {
this.set('collapsed', !this.get('collapsed'));
},
hasAnalyses: function () {
return this.getNumberOfAnalyses() > 0;
},
hasAggregatedStyles: function () {
return this.styleModel && this.styleModel.isAggregatedType();
},
getNumberOfAnalyses: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
var count = 0;
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
analysisNode = analysisNode.getPrimarySource();
if (analysisNode) {
count += 1;
}
}
return count;
},
getQualifiedTableName: function () {
var userName = this.get('user_name') || this.collection.userModel.get('username');
return TableNameUtils.getQualifiedTableName(
this.getTableName(),
userName,
this.collection.userModel.isInsideOrg()
);
},
getColumnNamesFromSchema: function () {
return this._getQuerySchemaModel().getColumnNames();
},
_getQuerySchemaModel: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
return nodeDefModel.querySchemaModel;
},
isDataLayer: function () {
var layerType = this.get('type');
return layerTypesAndKinds.isCartoDBType(layerType) ||
layerTypesAndKinds.isTorqueType(layerType);
},
isTorqueLayer: function () {
return this.get('type') === 'torque';
},
isAutoStyleApplied: function () {
var autoStyle = this.get('autoStyle');
return (autoStyle != null && autoStyle !== false);
},
_canBeFoldedUnderAnotherLayer: function () {
var thisNodeDefModel = this.getAnalysisDefinitionNodeModel();
return this.collection.any(function (m) {
if (m !== this && m.isDataLayer()) {
var otherNodeDefModel = m.getAnalysisDefinitionNodeModel();
if (otherNodeDefModel === thisNodeDefModel) return true;
var lastNode = _.last(otherNodeDefModel.linkedListBySameLetter());
return lastNode.getPrimarySource() === thisNodeDefModel;
}
}, this);
},
_isAllDataLayersDependingOnAnyAnalysisOfThisLayer: function () {
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
if (!nodeDefModel) return false;
if (!this.isOwnerOfAnalysisNode(nodeDefModel)) return false;
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
return this.collection.chain()
.filter(function (m) {
return m !== this && !!m.get('source');
}, this)
.all(function (m) {
return _.any(linkedNodesList, function (node) {
return m.containsNode(node);
});
}, this)
.value();
},
getAllDependentLayers: function () {
var self = this;
var layersCount = 0;
var layerDefinitionsCollectionModels = self.collection.models;
for (var i = 0; i < layerDefinitionsCollectionModels.length; i++) {
var layer = layerDefinitionsCollectionModels[i];
var dependentAnalysis = false;
if (layer !== self) {
var analysisNode = layer.getAnalysisDefinitionNodeModel();
while (analysisNode) {
if (self.isOwnerOfAnalysisNode(analysisNode)) {
dependentAnalysis = true;
}
analysisNode = analysisNode.getPrimarySource();
}
if (dependentAnalysis) {
layersCount += 1;
}
}
}
return layersCount;
},
matchesAttrs: function (otherAttrs) {
if (this.get('type') !== otherAttrs.type) {
return false;
}
if (layerTypesAndKinds.isTiledType(otherAttrs.type)) {
return this.get('name') === otherAttrs.name &&
this.get('urlTemplate') === otherAttrs.urlTemplate;
}
if (layerTypesAndKinds.isGMapsBase(otherAttrs.type)) {
return this.get('name') === otherAttrs.name &&
this.get('baseType') === otherAttrs.baseType &&
this.get('style') === otherAttrs.style;
}
if (layerTypesAndKinds.isPlainType(otherAttrs.type)) {
return this.get('color') === otherAttrs.color;
}
return false;
},
hasGeocodingAnalysisApplied: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
if (analysisNode && analysisNode.get('type') === 'geocoding') {
return true;
}
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
analysisNode = analysisNode.getPrimarySource();
if (analysisNode && analysisNode.get('type') === 'geocoding') {
return true;
}
}
return false;
},
_hasAnyAnalysisApplied: function () {
var analysisNode = this.getAnalysisDefinitionNodeModel();
return analysisNode.get('type') !== 'source';
},
isEmpty: function () {
throw new Error('LayerDefinitionModel.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
},
isEmptyAsync: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
return new Promise(function (resolve, reject) {
nodeModel.queryRowsCollection.isEmptyAsync()
.then(function (isEmpty) {
resolve(!hasAnyAnalysisApplied && !hasCustomQueryApplied && isEmpty);
});
});
},
isDataFiltered: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
return new Promise(function (resolve, reject) {
nodeModel.queryRowsCollection.isEmptyAsync()
.then(function (isEmpty) {
resolve((hasAnyAnalysisApplied || hasCustomQueryApplied) && isEmpty);
});
});
},
isDone: function () {
var nodeModel = this.getAnalysisDefinitionNodeModel();
return nodeModel.queryRowsCollection.isDone() &&
nodeModel.queryGeometryModel.isDone() &&
nodeModel.querySchemaModel.isDone();
},
canBeGeoreferenced: function () {
var self = this;
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
var emptyPromise = self.isEmptyAsync();
var geomPromise = analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync();
return Promise.all([emptyPromise, geomPromise])
.then(function (values) {
var isEmpty = values[0];
var hasGeom = values[1];
var canBeGeoreferenced =
!isEmpty &&
!hasGeom &&
!self.hasGeocodingAnalysisApplied() &&
!self._hasAnyAnalysisApplied() &&
!analysisDefinitionNodeModel.isCustomQueryApplied();
return canBeGeoreferenced;
});
},
fetchQueryRowsIfRequired: function () {
var self = this;
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync()
.then(function (geomHasValue) {
if (!self.hasGeocodingAnalysisApplied() &&
!geomHasValue &&
!analysisDefinitionNodeModel.isCustomQueryApplied() &&
analysisDefinitionNodeModel.queryRowsCollection.isUnavailable()) {
analysisDefinitionNodeModel.queryRowsCollection.fetch();
}
});
}
});