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

417 lines
12 KiB
JavaScript

var _ = require('underscore');
var Backbone = require('backbone');
var util = require('../core/util');
var Map = require('../geo/map');
var DataviewsFactory = require('../dataviews/dataviews-factory');
var AnalysisService = require('../analysis/analysis-service');
var LayersFactory = require('./layers-factory');
var SettingsModel = require('./settings');
var whenAllDataviewsFetched = require('./dataviews-tracker');
var RenderModes = require('../geo/render-modes');
var Engine = require('../engine');
var STATE_INIT = 'init'; // vis hasn't been sent to Windshaft
var STATE_OK = 'ok'; // vis has been sent to Windshaft and everything is ok
var STATE_ERROR = 'error'; // vis has been sent to Windshaft and there were some issues
var VisModel = Backbone.Model.extend({
defaults: {
loading: false,
showEmptyInfowindowFields: false,
showLimitErrors: false,
state: STATE_INIT
},
initialize: function () {
this._loadingObjects = [];
this.overlaysCollection = new Backbone.Collection();
this.settings = new SettingsModel();
this._instantiateMapWasCalled = false;
},
getStaticImageURL: function (options) {
options = _.defaults({}, options, {
zoom: 4,
lat: 0,
lng: 0,
width: 300,
height: 300,
format: 'png'
});
var url;
var urlTemplate = this.layerGroupModel.getStaticImageURLTemplate();
if (urlTemplate) {
url = urlTemplate
.replace('{z}', options.zoom)
.replace('{lat}', options.lat)
.replace('{lng}', options.lng)
.replace('{width}', options.width)
.replace('{height}', options.height)
.replace('{format}', options.format);
}
return url;
},
done: function (callback) {
this._doneCallback = callback;
return this;
},
setOk: function () {
// Invoke this._doneCallback if present, the first time
// the vis is instantiated correctly
if (this.get('state') === STATE_INIT) {
this._doneCallback && this._doneCallback(this);
}
this.set('state', STATE_OK);
this.unset('error');
},
error: function (callback) {
this._errorCallback = callback;
return this;
},
setError: function (error) {
// Invoke this._errorCallback if present, the first time
// the vis is instantiated and the're some errors
if (this.get('state') === STATE_INIT) {
this._errorCallback && this._errorCallback(error);
}
this.set({
state: STATE_ERROR,
error: error
});
},
/**
* @return Array of {LayerModel}
*/
getLayers: function () {
return _.clone(this.map.layers.models);
},
/**
* @param {Integer} index Layer index (including base layer if present)
* @return {LayerModel}
*/
getLayer: function (index) {
return this.map.layers.at(index);
},
load: function (vizjson) {
var windshaftSettings = {
urlTemplate: vizjson.datasource.maps_api_template,
userName: vizjson.datasource.user_name,
client: vizjson.datasource.client,
apiKey: this.get('apiKey'),
authToken: this.get('authToken'),
templateName: vizjson.datasource.template_name
};
this._engine = this._createEngine(windshaftSettings);
this._engine.on(Engine.Events.RELOAD_SUCCESS, this._onEngineReloadSuccess, this);
this._engine.on(Engine.Events.RELOAD_ERROR, this._onEngineReloadError, this);
this._engine.on(Engine.Events.LAYER_ERROR, this._onEngineLayerError, this);
// Bind layerGroupModel object to engine
this.layerGroupModel = this._engine._cartoLayerGroup;
// Create the public Analysis Service
this._analysisService = new AnalysisService({
engine: this._engine,
apiKey: windshaftSettings.apiKey,
authToken: windshaftSettings.authToken
});
// Public wrapper exposing public methods.
this.analysis = {
analyse: this._analysisService.analyse.bind(this._analysisService),
findNodeById: this._analysisService.findNodeById.bind(this._analysisService)
};
var allowScrollInOptions = (vizjson.options && vizjson.options.scrollwheel) || vizjson.scrollwheel;
var allowDragging = util.isMobileDevice() || vizjson.hasZoomOverlay() || allowScrollInOptions;
var renderMode = RenderModes.AUTO;
if (vizjson.vector === true) {
renderMode = RenderModes.VECTOR;
} else if (vizjson.vector === false) {
renderMode = RenderModes.RASTER;
}
this.layersFactory = new LayersFactory({ engine: this._engine, windshaftSettings: windshaftSettings });
this.map = new Map({
title: vizjson.title,
description: vizjson.description,
bounds: vizjson.bounds,
center: vizjson.center,
zoom: vizjson.zoom,
scrollwheel: !!allowScrollInOptions,
drag: allowDragging,
provider: vizjson.map_provider,
isFeatureInteractivityEnabled: this.get('interactiveFeatures'),
renderMode: renderMode
}, {
layersCollection: this._layersCollection,
layersFactory: this.layersFactory
});
this.listenTo(this.map, 'cartodbLayerMoved', this.reload);
// Reset the collection of overlays
this.overlaysCollection.reset(vizjson.overlays);
// Create the public Dataview Factory
// TODO: create dataviews more explicitly
this.dataviews = new DataviewsFactory({}, {
map: this.map,
engine: this._engine,
dataviewsCollection: this._dataviewsCollection
});
// Create layers
var analysisNodes = this._createAnalysisNodes(vizjson.analyses);
var layerModels = this._createLayers(vizjson.layers, analysisNodes);
this.map.layers.reset(layerModels);
// Global variable for easier console debugging / testing
window.vis = this;
_.defer(function () {
this.trigger('load', this);
}.bind(this));
},
_createEngine: function (windshaftSettings) {
var engine = new Engine({
apiKey: windshaftSettings.apiKey,
authToken: windshaftSettings.authToken,
username: windshaftSettings.userName,
serverUrl: windshaftSettings.urlTemplate,
templateName: windshaftSettings.templateName,
client: windshaftSettings.client
});
// TODO: Use engine.layerscollection in every reference
this._layersCollection = engine._layersCollection;
this._dataviewsCollection = engine._dataviewsCollection;
return engine;
},
/**
* Return the engine for this visModel
*/
getEngine: function () {
return this._engine;
},
// we provide a method to set some new settings
setSettings: function (settings) {
this.settings.set(settings);
},
/**
* Check if an analysis is the source of any layer.
*/
_isAnalysisLinkedToLayer: function (analysisModel) {
return this._layersCollection.any(function (layerModel) {
return layerModel.hasSource(analysisModel);
});
},
trackLoadingObject: function (object) {
if (this._loadingObjects.indexOf(object) === -1) {
this._loadingObjects.push(object);
}
this.set('loading', true);
},
untrackLoadingObject: function (object) {
var index = this._loadingObjects.indexOf(object);
if (index >= 0) {
this._loadingObjects.splice(index, 1);
if (this._loadingObjects.length === 0) {
this.set('loading', false);
}
}
},
/**
* Force a map instantiation.
* Only expected to be called once if {skipMapInstantiation} flag is set to true when vis is created.
*/
instantiateMap: function (options) {
options = options || {};
if (this._instantiateMapWasCalled) {
return;
}
this._instantiateMapWasCalled = true;
this.reload({
success: function () {
this._onMapInstantiatedForTheFirstTime();
options.success && options.success();
}.bind(this),
error: function (error) {
options.error && options.error(error && error.message);
},
includeFilters: false
});
},
_onMapInstantiatedForTheFirstTime: function () {
var anyDataviewFiltered = this._isAnyDataviewFiltered();
whenAllDataviewsFetched(this._dataviewsCollection, this._onDataviewFetched.bind(this));
this._initBindsAfterFirstMapInstantiation();
anyDataviewFiltered && this.reload({ includeFilters: anyDataviewFiltered });
},
_isAnyDataviewFiltered: function () {
return this._dataviewsCollection.isAnyDataviewFiltered();
},
_onDataviewFetched: function () {
this.trigger('dataviewsFetched');
},
_onEngineReloadSuccess: function () {
this.trigger('reloaded');
var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection);
this._isAnyAnalysisNodeLoading(analysisNodes) ? this.trackLoadingObject(this) : this.untrackLoadingObject(this);
this.setOk();
},
_onEngineReloadError: function (error) {
if (error && error.isGlobalError && error.isGlobalError()) {
this.setError(error);
}
},
_onEngineLayerError: function (error) {
if (error) {
this.map.trigger('error:' + error.type, error);
}
},
reload: function (options) {
if (this._instantiateMapWasCalled) {
this.trigger('reload');
return this._engine.reload(options);
}
return Promise.resolve();
},
_isAnyAnalysisNodeLoading: function (analysisNodes) {
return _.any(analysisNodes, function (analysisModel) {
return analysisModel.isLoading();
});
},
_initBindsAfterFirstMapInstantiation: function () {
this._layersCollection.bind('reset', this._onLayersResetted, this);
this._layersCollection.bind('add', this._onLayerAdded, this);
this._layersCollection.bind('remove', this._onLayerRemoved, this);
if (this._dataviewsCollection) {
// When new dataviews are defined, a new instance of the map needs to be created
this._dataviewsCollection.on('add reset remove', _.debounce(this.invalidateSize, 10), this);
this.listenTo(this._dataviewsCollection, 'add', _.debounce(this._onDataviewAdded.bind(this), 10));
this.listenTo(this._dataviewsCollection, 'remove', this._onDataviewRemoved);
}
},
_onDataviewRemoved: function (dataviewModel) {
if (dataviewModel.isFiltered()) {
this.reload({
sourceId: dataviewModel.getSourceId()
});
}
},
_onLayersResetted: function () {
this.reload();
},
_onLayerAdded: function (layerModel, collection, opts) {
opts = opts || {};
if (!opts.silent) {
this.reload({
sourceId: layerModel.get('id')
});
}
},
_onLayerRemoved: function (layerModel, collection, opts) {
opts = opts || {};
if (!opts.silent) {
this.reload({
sourceId: layerModel.get('id')
});
}
},
_onDataviewAdded: function () {
this.reload();
},
invalidateSize: function () {
this.trigger('invalidateSize');
},
addCustomOverlay: function (overlayView) {
overlayView.type = 'custom';
this.overlaysCollection.add(overlayView);
return overlayView;
},
isLoading: function () {
return this.get('loading');
},
/**
* "Load" existing analyses from the viz.json. This will generate
* the analyses graphs and index analysis nodes in the
* collection of analysis
*/
_createAnalysisNodes: function (analysesDefinition) {
_.each(analysesDefinition, function (analysisDefinition) {
this._analysisService.analyse(analysisDefinition);
}, this);
},
_createLayers: function (layersDefinition, analysisNodes) {
var layers = _.map(layersDefinition, function (layerData, layerIndex) {
// Flatten "options" and set the "order" attribute
layerData = _.extend({},
_.omit(layerData, 'options'),
layerData.options, {
order: layerIndex
}
);
if (layerData.source) {
layerData.source = this._analysisService.findNodeById(layerData.source);
} else {
// TODO: We'll be able to remove this (accepting sql option) once
// https://github.com/CartoDB/cartodb.js/issues/1754 is closed.
if (layerData.sql) {
layerData.source = this._analysisService.createAnalysisForLayer(layerData.id, layerData.sql);
delete layerData.sql;
}
}
return this.layersFactory.createLayer(layerData.type, layerData);
}, this);
return layers;
}
});
module.exports = VisModel;