CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/data/layer-definition-model.js

Summary

Maintainability
D
2 days
Test Coverage
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();
        }
      });
  }
});