490 lines
15 KiB
JavaScript
490 lines
15 KiB
JavaScript
|
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();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|