491 lines
12 KiB
JavaScript
491 lines
12 KiB
JavaScript
var _ = require('underscore');
|
|
var Backbone = require('backbone');
|
|
var status = require('../constants').status;
|
|
var SourceBase = require('../source/base');
|
|
var FilterBase = require('../filter/base');
|
|
var SQLFilterBase = require('../filter/base-sql');
|
|
var SpatialFilterTypes = require('../filter/spatial-filter-types');
|
|
var CartoError = require('../error-handling/carto-error');
|
|
var CartoValidationError = require('../error-handling/carto-validation-error');
|
|
|
|
/**
|
|
* Base class for dataview objects.
|
|
*
|
|
* Dataviews are a way to extract data from a CARTO account in predefined ways
|
|
* (eg: a list of categories, the result of a formula operation, etc.).
|
|
*
|
|
* **This object should not be used directly**
|
|
*
|
|
* The data used in a dataviews cames from a {@link carto.source.Base|source} that might change
|
|
* due to different reasons (eg: SQL query changed).
|
|
*
|
|
* When dataview data changes the dataview will trigger events to notify subscribers when new data is available.
|
|
*
|
|
* @example
|
|
* // Keep your widget data sync. Remember each dataview has his own data format.
|
|
* dataview.on('dataChanged', newData => {
|
|
* renderWidget(newData);
|
|
* })
|
|
*
|
|
* @constructor
|
|
* @abstract
|
|
* @memberof carto.dataview
|
|
* @fires dataChanged
|
|
* @fires columnChanged
|
|
* @fires statusChanged
|
|
* @fires error
|
|
* @api
|
|
*/
|
|
function Base () { }
|
|
|
|
_.extend(Base.prototype, Backbone.Events);
|
|
|
|
/**
|
|
* Return the current dataview status.
|
|
*
|
|
* @return {carto.dataview.status} Current dataview status
|
|
* @api
|
|
*/
|
|
Base.prototype.getStatus = function () {
|
|
return this._status;
|
|
};
|
|
|
|
/**
|
|
* Return true is the current status is loading.
|
|
*
|
|
* @return {boolean}
|
|
* @api
|
|
*/
|
|
Base.prototype.isLoading = function () {
|
|
return this._status === status.LOADING;
|
|
};
|
|
|
|
/**
|
|
* Return true is the current status is loaded.
|
|
*
|
|
* @return {boolean}
|
|
* @api
|
|
*/
|
|
Base.prototype.isLoaded = function () {
|
|
return this._status === status.LOADED;
|
|
};
|
|
|
|
/**
|
|
* Return true is the current status is error.
|
|
*
|
|
* @return {boolean}
|
|
* @api
|
|
*/
|
|
Base.prototype.hasError = function () {
|
|
return this._status === status.ERROR;
|
|
};
|
|
|
|
/**
|
|
* Enable the dataview. When enabled, a dataview fetches new data
|
|
* when the map changes (changing map configuration or changing map
|
|
* bounding box).
|
|
*
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.enable = function () {
|
|
return this._setEnabled(true);
|
|
};
|
|
|
|
/**
|
|
* Disable the dataview. This stops the dataview from fetching new
|
|
* data when there is a map change (like changing map configuration or changing map
|
|
* bounding box).
|
|
*
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.disable = function () {
|
|
return this._setEnabled(false);
|
|
};
|
|
|
|
/**
|
|
* Return true if the dataview is enabled.
|
|
*
|
|
* @return {boolean}
|
|
* @api
|
|
*/
|
|
Base.prototype.isEnabled = function () {
|
|
return this._enabled;
|
|
};
|
|
|
|
/**
|
|
* Return the current source where the dataview gets the data from.
|
|
*
|
|
* @return {carto.source.Base} Current source object
|
|
* @api
|
|
*/
|
|
Base.prototype.getSource = function () {
|
|
return this._source;
|
|
};
|
|
|
|
/**
|
|
* Set the dataview column.
|
|
*
|
|
* @param {string} column
|
|
* @fires columnChanged
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.setColumn = function (column) {
|
|
this._checkColumn(column);
|
|
this._column = column;
|
|
if (this._internalModel) {
|
|
this._internalModel.set('column', this._column);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Return the current dataview column where the dataview is applied.
|
|
*
|
|
* @return {string} Current dataview column
|
|
* @api
|
|
*/
|
|
Base.prototype.getColumn = function () {
|
|
return this._column;
|
|
};
|
|
|
|
/**
|
|
* Add a {@link carto.filter.Base|filter}.
|
|
*
|
|
* @param {carto.filter.Base} filter
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.addFilter = function (filter) {
|
|
this._checkFilter(filter);
|
|
this._addSpatialFilter(filter);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove a {@link carto.filter.Base|filter}.
|
|
*
|
|
* @param {carto.filter.Base} filter
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.removeFilter = function (filter) {
|
|
this._checkFilter(filter);
|
|
this._removeSpatialFilter(filter);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Check if a {@link carto.filter.Base|filter} exists in the dataview.
|
|
*
|
|
* @param {carto.filter.Base} filter
|
|
* @return {carto.dataview.Base} this
|
|
* @api
|
|
*/
|
|
Base.prototype.hasFilter = function (filter) {
|
|
this._checkFilter(filter);
|
|
var hasBBoxFilter = (filter === this._boundingBoxFilter) &&
|
|
(this._internalModel && this._internalModel.get('sync_on_bbox_change'));
|
|
|
|
var hasCircleFilter = (filter === this._circleFilter) &&
|
|
(this._internalModel && this._internalModel.get('sync_on_circle_change'));
|
|
|
|
var hasPolygonFilter = (filter === this._polygonFilter) &&
|
|
(this._internalModel && this._internalModel.get('sync_on_polygon_change'));
|
|
|
|
return hasBBoxFilter || hasCircleFilter || hasPolygonFilter;
|
|
};
|
|
|
|
Base.prototype.getData = function () {
|
|
throw new Error('getData must be implemented by the particular dataview.');
|
|
};
|
|
|
|
// Protected methods
|
|
|
|
Base.prototype.DEFAULTS = {};
|
|
|
|
/**
|
|
* Initialize dataview.
|
|
*
|
|
* @param {carto.source.Base} source - The source where the dataview will fetch the data
|
|
* @param {string} column - The column name to get the data
|
|
* @param {object} options - It depends on the instance
|
|
*/
|
|
Base.prototype._initialize = function (source, column, options) {
|
|
options = _.defaults(options || {}, this.DEFAULTS);
|
|
|
|
this._checkSource(source);
|
|
this._checkColumn(column);
|
|
this._checkOptions(options);
|
|
|
|
this._source = source;
|
|
this._column = column;
|
|
this._options = options;
|
|
|
|
this._status = status.NOT_LOADED;
|
|
this._enabled = true;
|
|
this._boundingBoxFilter = null;
|
|
};
|
|
|
|
Base.prototype._checkSource = function (source) {
|
|
if (!(source instanceof SourceBase)) {
|
|
throw this._getValidationError('sourceRequired');
|
|
}
|
|
};
|
|
|
|
Base.prototype._checkColumn = function (column) {
|
|
if (_.isUndefined(column)) {
|
|
throw this._getValidationError('columnRequired');
|
|
}
|
|
if (!_.isString(column)) {
|
|
throw this._getValidationError('columnString');
|
|
}
|
|
if (_.isEmpty(column)) {
|
|
throw this._getValidationError('emptyColumn');
|
|
}
|
|
};
|
|
|
|
Base.prototype._checkOptions = function (options) {
|
|
throw new Error('_checkOptions must be implemented by the particular dataview.');
|
|
};
|
|
|
|
Base.prototype._checkFilter = function (filter) {
|
|
if (!(filter instanceof FilterBase) || filter instanceof SQLFilterBase) {
|
|
throw this._getValidationError('filterRequired');
|
|
}
|
|
};
|
|
|
|
Base.prototype._createInternalModel = function (engine) {
|
|
throw new Error('_createInternalModel must be implemented by the particular dataview.');
|
|
};
|
|
|
|
Base.prototype._setEnabled = function (enabled) {
|
|
this._enabled = enabled;
|
|
if (this._internalModel) {
|
|
this._internalModel.set('enabled', enabled);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Base.prototype._listenToInternalModelSharedEvents = function () {
|
|
if (this._internalModel) {
|
|
this.listenTo(this._internalModel, 'change:data', this._onDataChanged);
|
|
this.listenTo(this._internalModel, 'change:column', this._onColumnChanged);
|
|
this.listenTo(this._internalModel, 'loading', this._onStatusLoading);
|
|
this.listenTo(this._internalModel, 'loaded', this._onStatusLoaded);
|
|
this.listenTo(this._internalModel, 'statusError', this._onStatusError);
|
|
}
|
|
};
|
|
|
|
Base.prototype._onDataChanged = function () {
|
|
this.trigger('dataChanged', this.getData());
|
|
};
|
|
|
|
Base.prototype._onColumnChanged = function () {
|
|
if (this._internalModel) {
|
|
this._column = this._internalModel.get('column');
|
|
}
|
|
this.trigger('columnChanged', this._column);
|
|
};
|
|
|
|
Base.prototype._onStatusLoading = function () {
|
|
this._status = status.LOADING;
|
|
this.trigger('statusChanged', this._status);
|
|
};
|
|
|
|
Base.prototype._onStatusLoaded = function () {
|
|
this._status = status.LOADED;
|
|
this.trigger('statusChanged', this._status);
|
|
};
|
|
|
|
Base.prototype._onStatusError = function (model, error) {
|
|
this._status = status.ERROR;
|
|
this.trigger('statusChanged', this._status, error);
|
|
this._triggerError(this, error);
|
|
};
|
|
|
|
Base.prototype._changeProperty = function (key, value, internalKey) {
|
|
var prevValue = this['_' + key];
|
|
this['_' + key] = value;
|
|
if (prevValue === value) {
|
|
return;
|
|
}
|
|
this._triggerChange(key, value);
|
|
if (this._internalModel) {
|
|
this._internalModel.set(internalKey || key, value);
|
|
}
|
|
};
|
|
|
|
Base.prototype._changeProperties = function (properties) {
|
|
_.each(properties, (value, key) => {
|
|
const prevValue = this[`_${key}`];
|
|
|
|
if (prevValue !== value) {
|
|
this[`_${key}`] = value;
|
|
this._triggerChange(key, value);
|
|
}
|
|
});
|
|
|
|
if (this._internalModel) {
|
|
this._internalModel.set(properties);
|
|
}
|
|
};
|
|
|
|
Base.prototype._triggerChange = function (key, value) {
|
|
this.trigger(key + 'Changed', value);
|
|
};
|
|
|
|
/**
|
|
* Fire a CartoError event from a internalDataviewError.
|
|
*/
|
|
Base.prototype._triggerError = function (model, internalDataviewError) {
|
|
this.trigger('error', new CartoError(internalDataviewError));
|
|
};
|
|
|
|
Base.prototype._addSpatialFilter = function (spatialFilter) {
|
|
switch (spatialFilter.type) {
|
|
case SpatialFilterTypes.BBOX:
|
|
this._addBoundingBoxFilter(spatialFilter);
|
|
break;
|
|
case SpatialFilterTypes.CIRCLE:
|
|
this._addCircleFilter(spatialFilter);
|
|
break;
|
|
case SpatialFilterTypes.POLYGON:
|
|
this._addPolygonFilter(spatialFilter);
|
|
break;
|
|
default:
|
|
throw new Error('The filter is not a valid spatial filter.');
|
|
}
|
|
};
|
|
|
|
Base.prototype._removeSpatialFilter = function (spatialFilter) {
|
|
switch (spatialFilter.type) {
|
|
case SpatialFilterTypes.BBOX:
|
|
if (spatialFilter === this._boundingBoxFilter) {
|
|
this._removeBoundingBoxFilter();
|
|
}
|
|
break;
|
|
case SpatialFilterTypes.CIRCLE:
|
|
if (spatialFilter === this._circleFilter) {
|
|
this._removeCircleFilter();
|
|
}
|
|
break;
|
|
case SpatialFilterTypes.POLYGON:
|
|
if (spatialFilter === this._polygonFilter) {
|
|
this._removePolygonFilter();
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error('The filter is not a valid spatial filter.');
|
|
}
|
|
};
|
|
|
|
Base.prototype._addBoundingBoxFilter = function (bboxFilter) {
|
|
if (bboxFilter === this._boundingBoxFilter) {
|
|
return;
|
|
}
|
|
|
|
this._boundingBoxFilter = bboxFilter;
|
|
if (this._internalModel) {
|
|
this._internalModel.addBBoxFilter(this._boundingBoxFilter.$getInternalModel());
|
|
this._internalModel.set('sync_on_bbox_change', true);
|
|
}
|
|
};
|
|
|
|
Base.prototype._removeBoundingBoxFilter = function () {
|
|
this._boundingBoxFilter = null;
|
|
if (this._internalModel) {
|
|
this._internalModel.removeBBoxFilter();
|
|
this._internalModel.set('sync_on_bbox_change', false);
|
|
}
|
|
};
|
|
|
|
Base.prototype._addCircleFilter = function (circleFilter) {
|
|
if (circleFilter === this._circleFilter) {
|
|
return;
|
|
}
|
|
|
|
this._circleFilter = circleFilter;
|
|
if (this._internalModel) {
|
|
this._internalModel.addCircleFilter(this._circleFilter.$getInternalModel());
|
|
this._internalModel.set('sync_on_circle_change', true);
|
|
}
|
|
};
|
|
|
|
Base.prototype._removeCircleFilter = function () {
|
|
this._circleFilter = null;
|
|
if (this._internalModel) {
|
|
this._internalModel.removeCircleFilter();
|
|
this._internalModel.set('sync_on_circle_change', false);
|
|
}
|
|
};
|
|
|
|
Base.prototype._addPolygonFilter = function (polygonFilter) {
|
|
if (polygonFilter === this._polygonFilter) {
|
|
return;
|
|
}
|
|
|
|
this._polygonFilter = polygonFilter;
|
|
if (this._internalModel) {
|
|
this._internalModel.addPolygonFilter(this._polygonFilter.$getInternalModel());
|
|
this._internalModel.set('sync_on_polygon_change', true);
|
|
}
|
|
};
|
|
|
|
Base.prototype._removePolygonFilter = function () {
|
|
this._polygonFilter = null;
|
|
if (this._internalModel) {
|
|
this._internalModel.removePolygonFilter();
|
|
this._internalModel.set('sync_on_polygon_change', false);
|
|
}
|
|
};
|
|
|
|
Base.prototype._getValidationError = function (code) {
|
|
return new CartoValidationError('dataview', code);
|
|
};
|
|
|
|
// Internal public methods
|
|
|
|
Base.prototype.$setEngine = function (engine) {
|
|
this._source.$setEngine(engine);
|
|
if (!this._internalModel) {
|
|
this._createInternalModel(engine);
|
|
this._listenToInternalModelSharedEvents();
|
|
}
|
|
};
|
|
|
|
Base.prototype.$getInternalModel = function () {
|
|
return this._internalModel;
|
|
};
|
|
|
|
module.exports = Base;
|
|
|
|
/**
|
|
* Fired when the column name has changed. Handler gets a parameter with the new column name.
|
|
*
|
|
* @event columnChanged
|
|
* @type {string}
|
|
* @api
|
|
*/
|
|
|
|
/**
|
|
* Fired when the status has changed. Handler gets a parameter with the new status.
|
|
*
|
|
* Contains a single argument with the new status.
|
|
*
|
|
* @event statusChanged
|
|
* @type {carto.dataview.status}
|
|
* @api
|
|
*/
|
|
|
|
/**
|
|
* Fired when the data has changed. Handler gets an object with specific data for the type
|
|
* of dataview that triggered the event.
|
|
*
|
|
* @event dataChanged
|
|
* @type {carto.dataview.CategoryData|carto.dataview.FormulaData|carto.dataview.HistogramData|carto.dataview.TimeSeriesData}
|
|
* @api
|
|
*/
|