Initial commit
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user