Files
carto.js/src/engine.js
2020-06-13 18:34:34 +08:00

464 lines
15 KiB
JavaScript

var _ = require('underscore');
var AnalysisPoller = require('./analysis/analysis-poller');
var AnonymousMapSerializer = require('./windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer');
var Backbone = require('backbone');
var CartoDBLayerGroup = require('./geo/cartodb-layer-group');
var DataviewsCollection = require('./dataviews/dataviews-collection');
var LayersCollection = require('./geo/map/layers');
var ModelUpdater = require('./windshaft-integration/model-updater');
var NamedMapSerializer = require('./windshaft/map-serializer/named-map-serializer/named-map-serializer');
var Request = require('./windshaft/request');
var Response = require('./windshaft/response');
var WindshaftClient = require('./windshaft/client');
var AnalysisService = require('./analysis/analysis-service');
var WindshaftError = require('./windshaft/error');
var RELOAD_DEBOUNCE_TIME_IN_MILIS = 100;
/**
*
* Creates a new Engine.
* An engine is the core of a carto app.
*
* With the help of external services the engine will:
*
* - Keep the state of the layers and dataviews.
* - Serialize the state and send requests to the server.
* - Parse the server response and update the internal models.
* - Notify errors or successful operations.
*
* @param {Object} params - The parameters to initialize the engine.
* @param {string} params.apiKey - Api key used to be autenticate in the windshaft server.
* @param {string} params.authToken - Token used to be autenticate in the windshaft server.
* @param {string} params.username - Name of the user registered in the windshaft server.
* @param {string} params.serverUrl - Url of the windshaft server.
* @param {boolean} params.templateName - While we dont remove named maps we must explicitly say when the map is named. Defaults to false.
* @param {boolean} params.client - Token used to get map view statistics.
* @constructor
*/
function Engine (params) {
if (!params) throw new Error('new Engine() called with no parameters');
this._isNamedMap = params.templateName !== undefined;
// Variables for the reload debounce
this._timeout = null;
this._stackCalls = [];
this._batchOptions = {};
this._windshaftSettings = {
urlTemplate: params.serverUrl,
userName: params.username,
client: params.client,
apiKey: params.apiKey,
authToken: params.authToken,
templateName: params.templateName
};
this._windshaftClient = new WindshaftClient(this._windshaftSettings);
// This object will be responsible of triggering the engine events.
this._eventEmmitter = _.extend({}, Backbone.Events);
this._analysisPoller = new AnalysisPoller();
this._layersCollection = new LayersCollection();
this._dataviewsCollection = new DataviewsCollection();
this._cartoLayerGroup = new CartoDBLayerGroup(
{ apiKey: params.apiKey, authToken: params.authToken },
{ layersCollection: this._layersCollection }
);
this._bindCartoLayerGroupError();
this._modelUpdater = new ModelUpdater({
dataviewsCollection: this._dataviewsCollection,
layerGroupModel: this._cartoLayerGroup,
layersCollection: this._layersCollection
});
}
/**
* Return the cartoLayergroup attached to the engine
*/
Engine.prototype.getLayerGroup = function () {
return this._cartoLayerGroup;
};
/**
* Returns the API key attached to the engine
*/
Engine.prototype.getApiKey = function () {
return this._windshaftSettings && this._windshaftSettings.apiKey;
};
/**
* Returns the Auth token attached to the engine
*/
Engine.prototype.getAuthToken = function () {
return this._windshaftSettings && this._windshaftSettings.authToken;
};
/**
* Bind a callback function to an event. The callback will be invoked whenever the event is fired.
*
* @param {string} event - The name of the event that triggers the callback execution.
* @param {function} callback - A function to be executed when the event is fired.
* @param {function} [context] - The context value for this when the callback is invoked.
* @example
* // Define a callback to be executed once the map is reloaded.
* function onReload(event) {
* console.log(event); // "reload-success"
* }
* // Attach the callback to the RELOAD_SUCCESS event.
* engine.on(Engine.Events.RELOAD_SUCCESS, onReload);
* // Call the reload method and wait.
* engine.reload();
*
*/
Engine.prototype.on = function (event, callback, context) {
this._eventEmmitter.on(event, callback, context);
};
/**
* Remove a previously-bound callback function from an event.
*
* @param {string} event - The name of the event that triggers the callback execution.
* @param {function} callback - A function callback to be removed when the event is fired.
* @param {function} [context] - The context value for this when the callback is invoked.
* @example
* // Remove the the `displayMap` listener function so it wont be executed anymore when the engine fires the `load` event.
* engine.off(Engine.Events.RELOAD_SUCCESS, onReload);
*
*/
Engine.prototype.off = function (event, callback, context) {
this._eventEmmitter.off(event, callback, context);
};
/**
* This is the most important function of the engine.
* Generate a payload from the current state, send it to the windshaft server
* and update the internal models with the server response.
*
* Once the response has arrived trigger a 'reload-succes' or 'reload-error' event.
*
* @param {string} options.sourceId - The sourceId triggering the reload event. This is usefull to prevent uneeded requests and save data.
* @param {boolean} options.forceFetch - Forces dataviews to fetch data from server after a reload
* @param {boolean} options.includeFilters - Boolean flag to control if the filters need to be added in the payload.
*
* @fires Engine#Engine:RELOAD_STARTED
* @fires Engine#Engine:RELOAD_SUCCESS
* @fires Engine#Engine:RELOAD_ERROR
*
*/
Engine.prototype.reload = function (options) {
options = options || {};
// Using a debouncer to optimize consecutive calls to reload the map.
// This allows to change multiple map parameters reloading the map only once,
// and therefore avoid the "You are over platform's limits" Windshaft error.
return new Promise(function (resolve, reject) {
this._batchOptions = _.pick({
sourceId: options.sourceId,
forceFetch: this._batchOptions.forceFetch || options.forceFetch,
includeFilters: options.includeFilters
}, _.negate(_.isUndefined));
this._stackCalls.push({
success: options.success,
error: options.error,
resolve: resolve,
reject: reject
});
var later = function () {
this._timeout = null;
this._performReload(this._batchOptions)
.then(function () {
// Resolve stacked callbacks and promises
this._stackCalls.forEach(function (call) {
call.success && call.success();
call.resolve();
});
// Reset stack
this._stackCalls = [];
this._batchOptions = {};
}.bind(this))
.catch(function (windshaftError) {
// Reject stacked callbacks and promises
this._stackCalls.forEach(function (call) {
call.error && call.error(windshaftError);
call.reject(windshaftError);
});
// Reset stack
this._stackCalls = [];
this._batchOptions = {};
}.bind(this));
}.bind(this);
clearTimeout(this._timeout);
this._timeout = setTimeout(later, RELOAD_DEBOUNCE_TIME_IN_MILIS);
}.bind(this));
};
Engine.prototype._performReload = function (options) {
return new Promise(function (resolve, reject) {
// Build Windshaft options callbacks
var windshaftOptions = this._buildWindshaftOptions(options,
// Windshaft success callback
function (serverResponse) {
this._onReloadSuccess(serverResponse, options.sourceId, options.forceFetch);
resolve();
}.bind(this),
// Windshaft error callback
function (errors) {
var windshaftError = this._onReloadError(errors);
reject(windshaftError);
}.bind(this)
);
try {
var params = this._buildParams(windshaftOptions.includeFilters);
var payload = this._getSerializer().serialize(this._layersCollection, this._dataviewsCollection);
var request = new Request(payload, params, windshaftOptions);
// Trigger STARTED event
this._eventEmmitter.trigger(Engine.Events.RELOAD_STARTED);
// Perform the request
this._windshaftClient.instantiateMap(request);
} catch (error) {
// Convert error in a windshaftError
var windshaftError = new WindshaftError({ message: error.message });
this._manageClientError(windshaftError, windshaftOptions);
}
}.bind(this));
};
/**
*
* Add a layer to the engine layersCollection
*
* @param {layer} layer - A new layer to be added to the engine.
*
* @public
*/
Engine.prototype.addLayer = function (layer) {
this._layersCollection.add(layer);
};
/**
*
* Remove a layer from the engine layersCollection
*
* @param {layer} layer - A new layer to be removed from the engine.
*
* @public
*/
Engine.prototype.removeLayer = function (layer) {
this._layersCollection.remove(layer);
};
/**
*
* Move a layer in the engine layersCollection
*
* @param {layer} layer - A new layer to be moved in the engine.
* @param {number} toIndex - Final index for the layer.
*
* @public
*/
Engine.prototype.moveLayer = function (layer, toIndex) {
var fromIndex = this._layersCollection.indexOf(layer);
if (fromIndex >= 0 && fromIndex !== toIndex) {
this._layersCollection.models.splice(toIndex, 0, this._layersCollection.models.splice(fromIndex, 1)[0]);
// Equivalent to:
// this._layersCollection.remove(layer, { silent: true });
// this._layersCollection.add(layer, { at: toIndex });
}
};
/**
*
* Add a dataview to the engine dataviewsCollection
*
* @param {Dataview} dataview - A new dataview to be added to the engine.
*
* @public
*/
Engine.prototype.addDataview = function (dataview) {
this._dataviewsCollection.add(dataview);
};
/**
*
* Remove a dataview from the engine dataviewsCollection
*
* @param {Dataview} dataview - The Dataview to be removed to the engine.
*
* @public
*/
Engine.prototype.removeDataview = function (dataview) {
this._dataviewsCollection.remove(dataview);
};
/**
* Callback executed when the windhsaft client returns a successful response.
* Update internal models and trigger a RELOAD_SUCCESS event.
* @private
*/
Engine.prototype._onReloadSuccess = function (serverResponse, sourceId, forceFetch) {
var responseWrapper = new Response(this._windshaftSettings, serverResponse);
this._modelUpdater.updateModels(responseWrapper, sourceId, forceFetch);
this._restartAnalysisPolling();
// Trigger RELOAD_SUCCESS event
this._eventEmmitter.trigger(Engine.Events.RELOAD_SUCCESS);
};
/**
* Callback executed when the windhsaft client returns a failed response.
* Update internal models setting errors and trigger a RELOAD_ERROR event.
* @private
*/
Engine.prototype._onReloadError = function (errors) {
var windshaftError = this._getSimpleWindshaftError(errors);
this._modelUpdater.setErrors(errors);
// Trigger RELOAD_ERROR event
this._eventEmmitter.trigger(Engine.Events.RELOAD_ERROR, windshaftError);
return windshaftError;
};
/**
* Helper to get windhsaft request options.
* @private
*/
Engine.prototype._buildWindshaftOptions = function (options, successCallback, errorCallback) {
return _.extend({
includeFilters: true,
success: successCallback,
error: errorCallback
}, _.pick(options, 'sourceId', 'forceFetch', 'includeFilters'));
};
/**
* Helper to get windhsaft request parameters.
* @param {boolean} includeFilters - Boolean flag to control if the filters need to be added in the payload.
* @private
*/
Engine.prototype._buildParams = function (includeFilters) {
var params = {};
if (__ENV__ === 'production') {
params.client = this._windshaftSettings.client;
}
if (includeFilters && !_.isEmpty(this._dataviewsCollection.getFilters())) {
params.filters = this._dataviewsCollection.getFilters();
}
if (this._windshaftSettings.apiKey) {
params.api_key = this._windshaftSettings.apiKey;
return params;
}
if (this._windshaftSettings.authToken) {
params.auth_token = this._windshaftSettings.authToken;
return params;
}
console.warn('Engine initialized with no apiKeys neither authToken');
};
/**
* Reset the analysis nodes in the poller
* @private
*/
Engine.prototype._restartAnalysisPolling = function () {
var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection);
this._analysisPoller.resetAnalysisNodes(analysisNodes);
};
/**
* Get the instance of the serializer service depending on is an anonymous or a named map.
* @private
*/
Engine.prototype._getSerializer = function () {
return this._isNamedMap ? NamedMapSerializer : AnonymousMapSerializer;
};
/**
* Manage and propagate the client error
* @private
*/
Engine.prototype._manageClientError = function (windshaftError, windshaftOptions) {
this._modelUpdater.setErrors([windshaftError]);
windshaftOptions.error && windshaftOptions.error([windshaftError]);
};
/**
* Listen to errors in cartoLayerGroup
*/
Engine.prototype._bindCartoLayerGroupError = function () {
this._cartoLayerGroup.on('all', function (change, error) {
if (change.lastIndexOf('error:', 0) === 0) {
error = new WindshaftError(error);
this._eventEmmitter.trigger(Engine.Events.LAYER_ERROR, error);
}
}, this);
};
Engine.prototype._getSimpleWindshaftError = function (errors) {
var error = _.find(errors, function (error) { return error.isGlobalError(); });
if (!error && errors && errors.length > 0) {
error = errors[0];
}
return error;
};
/**
* Events fired by the engine
*
* @readonly
* @enum {string}
*/
Engine.Events = {
/**
* Reload started event, fired every time the reload process starts.
*/
RELOAD_STARTED: 'reload-started',
/**
* Reload success event, fired every time the reload function succeed.
*/
RELOAD_SUCCESS: 'reload-success',
/**
* Reload error event, fired every time the reload function fails.
*/
RELOAD_ERROR: 'reload-error',
/**
* Error event, fired every time a tile or limit error happens.
*/
LAYER_ERROR: 'layer-error'
};
module.exports = Engine;
/**
* Reload started event, fired every time the reload process starts.
*
* @event Engine#Engine:RELOAD_STARTED
* @type {string}
*/
/**
* Reload success event, fired every time the reload function succeed.
*
* @event Engine#Engine:RELOAD_SUCCESS
* @type {string}
*/
/**
* Reload success event, fired every time the reload function fails.
*
* @event Engine#Engine:RELOAD_ERROR
* @type {string}
*/
/**
* Layer group error event, fired every time an error with layer group happends (tile or limit).
*
* @event Engine#Engine:LAYER_ERROR
* @type {string}
*/