417 lines
12 KiB
JavaScript
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;
|