CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/data/query-geometry-model.js

Summary

Maintainability
B
6 hrs
Test Coverage
var _ = require('underscore');
var Backbone = require('backbone');
var BaseModel = require('./query-base-model');
var STATUS = require('./query-base-status');
/* eslint-disable */
var template = _.template("" +
"SELECT CASE LOWER(ST_GeometryType(<%= geom_column %>)) " +
"     WHEN 'st_polygon' THEN 'polygon' " +
"     WHEN 'st_multipolygon' THEN 'polygon' " +
"     WHEN 'st_multilinestring' THEN 'line' " +
"     WHEN 'st_linestring' THEN 'line' " +
"     WHEN 'st_multipoint' THEN 'point' " +
"     WHEN 'st_point' THEN 'point' " +
"     ELSE '' " +
"  END AS the_geom FROM (<%= sql %>) __wrapped WHERE <%= geom_column %> IS NOT NULL"
/* eslint-enable */
);

var THE_GEOM_NOT_FOUND_ERROR = 'column \"the_geom\" does not exist';
var PARAMS = {
  sort_order: 'asc',
  rows_per_page: 40,
  page: 0
};
var GEOMETRIES = {
  LINE: 'line',
  POINT: 'point',
  POLYGON: 'polygon'
};

/**
 * Model to represent a schema of a SQL query.
 */
module.exports = BaseModel.extend({

  defaults: {
    query: '',
    status: STATUS.initial,
    simple_geom: '', // , 'point', 'polygon', 'line'
    ready: false // until true there's no data available on the table(s) used in the query
  },

  initialize: function (attrs, opts) {
    if (!opts.configModel) throw new Error('configModel is required');
    BaseModel.prototype.initialize.call(this, attrs, opts);

    this._configModel = opts.configModel;

    this._addChangeListener();
  },

  url: function () {
    return this._configModel.getSqlApiUrl();
  },

  fetch: function (opts) {
    if (!this.canFetch()) return;

    this.set('status', STATUS.fetching);

    opts = opts || {};
    opts.errorCallback = opts && (opts.originalError || opts.error);
    var successCallback = opts && (opts.success || opts.complete);

    opts.data = _.extend(
      opts.data || {},
      {
        api_key: this._configModel.get('api_key'),
        q: this._getSqlApiQueryParam(opts.geomColumnName)
      },
      PARAMS
    );

    opts.method = this._httpMethod();

    opts.success = function (model, response) {
      this._resetRepeatedError();
      if (successCallback) { successCallback(response); }
    }.bind(this);

    opts.error = function (model, response) {
      if (response && response.statusText !== 'abort') {
        var error = response.responseText ? JSON.parse(response.responseText).error : [];
        // in case we get an error of the_geom does not exists try with the_geom_webmercator to get the geometry type
        if (error.length === 1 && error[0] === THE_GEOM_NOT_FOUND_ERROR) {
          this.fetch(_.extend({}, _.omit(opts, 'success'), { originalError: opts.errorCallback, geomColumnName: 'the_geom_webmercator' }));
        } else {
          this._incrementRepeatedError();

          this.set({
            simple_geom: '',
            query_errors: error,
            status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
          });

          if (opts.errorCallback) {
            opts.errorCallback({
              status: response.status || 'Unknown',
              error: error
            });
          }
        }
      }
    }.bind(this);

    return Backbone.Model.prototype.fetch.call(this, opts);
  },

  parse: function (r) {
    var simpleGeom;

    _.some(r.rows, function (row) {
      return !!(simpleGeom = row.the_geom); // to stop when found a valid simple geom
    });

    return {
      status: STATUS.fetched,
      simple_geom: simpleGeom || ''
    };
  },

  hasValue: function () {
    throw new Error('QueryGeometryModel.hasValue() is an async operation. Use `.hasValueAsync` instead.');
  },

  hasValueAsync: function () {
    var self = this;
    if (this.isFetched()) {
      return Promise.resolve(!!this.get('simple_geom'));
    } else {
      return new Promise(function (resolve) {
        self.listenToOnce(self, 'inFinalStatus', function () {
          resolve(!!self.get('simple_geom'));
        });
      });
    }
  },

  _onChange: function () {
    this._removeChangeListener();

    if (!this.hasChanged('status') && this.get('status') === STATUS.fetching) {
      // If it is already fetching just redo the fetch with latest attrs
      // in case the query has changed
      if (this.hasChanged('query')) {
        this.fetch();
      }
    } else if (this.hasChanged('query') || this.hasChanged('ready')) {
      this.set('status', this.get('query') ? STATUS.unfetched : STATUS.unavailable, { silent: true });
    }

    this._addChangeListener();
  },

  _getSqlApiQueryParam: function (geomColumnName) {
    return template({
      geom_column: geomColumnName || 'the_geom',
      sql: this.get('query')
    });
  },

  isPolygon: function () {
    return this.get('simple_geom') === GEOMETRIES.POLYGON;
  }
});