CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/data/layer-definitions-collection.js

Summary

Maintainability
D
2 days
Test Coverage
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var LayerDefinitionModel = require('./layer-definition-model');
var layerLetters = require('./layer-letters');
var layerColors = require('./layer-colors');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var layerTypesAndKinds = require('./layer-types-and-kinds');
var SQLUtils = require('builder/helpers/sql-utils');
var TableNameUtils = require('builder/helpers/table-name-utils');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
  'analysisDefinitionNodesCollection',
  'configModel',
  'mapId',
  'stateDefinitionModel',
  'userModel'
];

var generateLabelsLayerName = function (layerName) {
  return layerName + ' Labels';
};

/**
 * Collection of layer definitions
 */
module.exports = Backbone.Collection.extend({

  initialize: function (models, opts) {
    checkAndBuildOpts(opts, REQUIRED_OPTS, this);
  },

  url: function () {
    var baseUrl = this._configModel.get('base_url');
    return baseUrl + '/api/v1/maps/' + this._mapId + '/layers';
  },

  parse: function (r) {
    return r.layers;
  },

  save: function (options) {
    this.each(function (layerDefModel, index) {
      layerDefModel.set('order', index, { silent: true });
    });
    Backbone.sync('update', this, options);
  },

  toJSON: function () {
    return {
      layers: Backbone.Collection.prototype.toJSON.apply(this, arguments)
    };
  },

  /**
   * Intended to be called from entry point, to make sure initial layers are taken into account
   */
  resetByLayersData: function (layersData) {
    this.reset(layersData, {
      silent: true,
      initialLetters: _.chain(layersData)
        .pluck('options')
        .pluck('letter')
        .compact()
        .value()
    });

    this._sanitizeLabels();
  },

  comparator: function (model) {
    return model.get('order');
  },

  model: function (d, opts) {
    var self = opts.collection;

    // Add data required for new editor if not set (e.g. a vis created on old editor doesn't contain letter and source)
    var o = _.clone(d.options) || {};
    var attrs = _.defaults(
      { options: o },
      _.omit(d, ['options']
      ));

    if (!isNaN(opts.at)) {
      attrs.options.order = opts.at;
    }

    if (layerTypesAndKinds.isKindDataLayer(attrs.kind)) {
      o.letter = o.letter || layerLetters.next(self._takenLetters(opts.initialLetters));
      o.color = o.color || layerColors.getColorForLetter(o.letter);

      // Create source attr if it does not already exist
      var sourceId = nodeIds.next(o.letter);

      if (o.table_name && (!o.source || o.source === sourceId)) {
        var tableName = TableNameUtils.getUnqualifiedName(o.table_name);
        var userName = o.user_name || TableNameUtils.getUsername(o.table_name);

        var qualifyTableName = (userName && self._configModel.get('user_name') !== userName) || self._userModel.isInsideOrg();
        tableName = TableNameUtils.getQualifiedTableName(o.table_name, userName, qualifyTableName);

        o.source = o.source || sourceId;
        o.query = o.query || SQLUtils.getDefaultSQLFromTableName(tableName);

        // Add analysis definition unless already existing
        self._analysisDefinitionNodesCollection.createSourceNode({
          id: sourceId,
          tableName: tableName,
          query: o.query
        });
      }
    }

    var parseAttrs = typeof opts.parse === 'boolean' ? opts.parse : true;
    var m = new LayerDefinitionModel(attrs, {
      parse: parseAttrs,
      collection: self,
      configModel: self._configModel
    });

    return m;
  },

  nextLetter: function () {
    return layerLetters.next(this._takenLetters());
  },

  findAnalysisDefinitionNodeModel: function (id) {
    return this._analysisDefinitionNodesCollection.get(id);
  },

  findOwnerOfAnalysisNode: function (nodeDefModel) {
    return this.find(function (layerDefModel) {
      return layerDefModel.isOwnerOfAnalysisNode(nodeDefModel);
    });
  },

  findPrimaryParentLayerToAnalysisNode: function (nodeDefModel, opts) {
    if (!nodeDefModel) return;

    opts = opts || {};
    if (!_.isArray(opts.exclude)) {
      opts.exclude = [opts.exclude];
    }
    var owner = this.findOwnerOfAnalysisNode(nodeDefModel);

    return this.find(function (layerDefModel) {
      if (layerDefModel === owner) return;
      if (_.contains(opts.exclude, layerDefModel)) return;

      var layerHeadNode = layerDefModel.getAnalysisDefinitionNodeModel();
      if (nodeDefModel === layerHeadNode) return true;

      if (layerHeadNode) {
        var primarySourceOfLastOwnNode = _.last(layerHeadNode.linkedListBySameLetter()).getPrimarySource();
        return nodeDefModel === primarySourceOfLastOwnNode;
      }
    });
  },

  isThereAnyGeometryData: function () {
    var dataLayers = this._getDataLayers();
    var allQueryGeometryModels = this._getAllQueryGeometryModels(dataLayers);
    var queryGeometryModelsValuePromises = _.map(allQueryGeometryModels, function (queryGeometryModel) {
      return queryGeometryModel.hasValueAsync();
    });

    return Promise.all(queryGeometryModelsValuePromises)
      .then(function (hasGeomValues) {
        return _.some(hasGeomValues);
      });
  },

  loadAllQueryGeometryModels: function (callback) {
    var promises = this.filter(function (layerDefModel) {
      return layerDefModel.isDataLayer();
    }).map(function (layerDefModel) {
      var queryGeometryModel = layerDefModel.getAnalysisDefinitionNodeModel().queryGeometryModel;
      var status = queryGeometryModel.get('status');
      var deferred = new $.Deferred();

      if (queryGeometryModel.isFetched()) {
        deferred.resolve();
      } else if (queryGeometryModel.canFetch()) {
        if (status !== 'fetching') {
          queryGeometryModel.fetch({
            success: function () {
              deferred.resolve();
            },
            error: function () {
              deferred.reject();
            }
          });
        } else {
          deferred.resolve();
        }
      } else {
        deferred.reject();
      }

      return deferred.promise();
    }, this);

    $.when.apply($, promises).done(callback);
  },

  isThereAnyTorqueLayer: function () {
    return this.any(function (model) {
      return layerTypesAndKinds.isTorqueType(model.get('type'));
    });
  },

  isThereAnyCartoDBLayer: function () {
    return this.any(function (model) {
      return layerTypesAndKinds.isCartoDBType(model.get('type'));
    });
  },

  anyContainsNode: function (nodeDefModel) {
    return this.any(function (layerDefModel) {
      return layerDefModel.containsNode(nodeDefModel);
    });
  },

  isDataLayerOnTop: function (lyrModel) {
    var currentPos = this.indexOf(lyrModel);
    return currentPos === this.getTopDataLayerIndex();
  },

  getTopDataLayerIndex: function () {
    var layerOnTop = this.getLayerOnTop();
    var at = this.indexOf(layerOnTop);
    var hasLabels = layerOnTop &&
      !layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
      !layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));

    if (hasLabels) { at--; }

    return at;
  },

  getNumberOfDataLayers: function () {
    return this.select(function (model) {
      return layerTypesAndKinds.isTypeDataLayer(model.get('type'));
    }).length;
  },

  _takenLetters: function (otherLetters) {
    var valuesFromAddedModels = _.compact(this.pluck('letter'));

    // When adding multiple items the models created so far are stored in the internal object this._byId,
    // need to make sure to take them into account when returning already taken letter.
    var valuesFromModelsNotYetAdded = _.chain(this._byId).values().invoke('get', 'letter').value();

    return _.union(valuesFromAddedModels, valuesFromModelsNotYetAdded, otherLetters);
  },

  // This ensures that we don't end up with more than one labels layer
  _sanitizeLabels: function () {
    var troubled = this.models.filter(function (layer) {
      return layerTypesAndKinds.isTiledType(layer.get('type'));
    }).filter(function (layer) {
      var index = this.models.indexOf(layer);
      return (index > 0 && index < this.length - 1);
    }.bind(this));

    troubled.length > 0 && _.each(troubled, function (layer) {
      layer.destroy();
    });
  },

  setBaseLayer: function (newBaseLayerAttrs) {
    this.trigger('changingBaseLayer');

    newBaseLayerAttrs = _.clone(newBaseLayerAttrs);
    if (this.isBaseLayerAdded(newBaseLayerAttrs)) {
      this.trigger('baseLayerChanged');
      return false;
    }

    var newBaseLayerHasLabels = !!(newBaseLayerAttrs.labels && newBaseLayerAttrs.labels.urlTemplate);

    var labelsLayerOptions = {
      silent: true,
      wait: true, // do not add/remove the layer unless it's created/removed/updated successfully
      success: function () {
        this._sanitizeLabels();
        this.trigger('baseLayerChanged');
      }.bind(this),
      error: function () {
        this.trigger('baseLayerFailed');
      }.bind(this)
    };

    var baseLayerOptions = {
      silent: true,
      wait: true, // do not update the layer unless it's created/updated successfully
      success: function () {
        var labelsLayer = this.getLabelsLayer();
        if (newBaseLayerHasLabels) {
          if (labelsLayer) {
            this._updateLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
          } else {
            this._addLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
          }
        } else {
          if (labelsLayer) {
            this._destroyLabelsLayer(labelsLayerOptions);
          } else {
            this._sanitizeLabels();
            this.trigger('baseLayerChanged');
          }
        }
      }.bind(this),
      error: function () {
        this.trigger('baseLayerFailed');
      }.bind(this)
    };

    if (this.getBaseLayer()) {
      this._updateBaseLayer(newBaseLayerAttrs, baseLayerOptions);
    } else {
      this._createBaseLayer(newBaseLayerAttrs, baseLayerOptions);
    }
  },

  isBaseLayerAdded: function (basemapAttrs) {
    var baseLayerDefinitionModel = this.getBaseLayer();
    return baseLayerDefinitionModel.matchesAttrs(basemapAttrs);
  },

  _updateBaseLayer: function (basemapAttrs, opts) {
    var currentBaseLayer = this.getBaseLayer();
    var newBaseLayerAttrs = _.omit(basemapAttrs, [ 'id', 'order', 'labels', 'template' ]);

    currentBaseLayer.attributes = _.extend({
      order: 0
    }, _.pick(currentBaseLayer.attributes, [
      'id',
      'type',
      'category'
    ]));

    currentBaseLayer.save(newBaseLayerAttrs, opts);
  },

  _createBaseLayer: function (newBaseLayerAttrs, opts) {
    newBaseLayerAttrs = _.extend(newBaseLayerAttrs, {
      order: 0
    });
    return this.create(newBaseLayerAttrs, _.extend({ at: 0 }, opts));
  },

  _updateLabelsLayer: function (newBaseLayerAttrs, opts) {
    var labelsLayer = this.getLabelsLayer();
    var newAttrs = _.omit(newBaseLayerAttrs, 'labels');

    newAttrs = _.pick(newAttrs, _.identity);
    newAttrs = _.extend(newAttrs, newBaseLayerAttrs.labels, {
      name: generateLabelsLayerName(newBaseLayerAttrs.name)
    });

    labelsLayer.save(newAttrs, opts);
  },

  _addLabelsLayer: function (newBaseLayerAttrs, opts) {
    var optionsAttribute = _.pick(newBaseLayerAttrs, ['type', 'subdomains', 'minZoom', 'maxZoom', 'name', 'className', 'attribution', 'category']);
    optionsAttribute = _.extend(optionsAttribute, newBaseLayerAttrs.labels, {
      name: generateLabelsLayerName(newBaseLayerAttrs.name)
    });

    var labelsLayerAttrs = {
      order: this.getLayerOnTop().get('order') + 1,
      options: optionsAttribute
    };

    this.create(labelsLayerAttrs, opts);
  },

  _destroyLabelsLayer: function (opts) {
    this.getLayerOnTop().destroy(opts);
  },

  _getAllQueryGeometryModels: function (dataLayers) {
    return _.compact(_.map(dataLayers, function (dataLayer) {
      return dataLayer.getAnalysisDefinitionNodeModel().queryGeometryModel;
    }));
  },

  _getDataLayers: function () {
    return this.filter(function (layerDefinitionModel) {
      return layerDefinitionModel.isDataLayer();
    });
  },

  getBaseLayer: function () {
    return this.first();
  },

  getLabelsLayer: function () {
    var layerOnTop = this.getLayerOnTop();
    if (this.size() > 1 && layerTypesAndKinds.isTiledType(layerOnTop.get('type'))) {
      return layerOnTop;
    }
  },

  getLayerOnTop: function () {
    return this.last();
  }
});