cartodb/lib/assets/javascripts/builder/deep-insights-integration/layers-integration.js
2020-06-15 10:58:47 +08:00

396 lines
14 KiB
JavaScript

var _ = require('underscore');
var Backbone = require('backbone');
var LegendManager = require('./legend-manager');
var linkLayerInfowindow = require('./link-layer-infowindow');
var linkLayerTooltip = require('./link-layer-tooltip');
var layerTypesAndKinds = require('builder/data/layer-types-and-kinds');
var Notifier = require('builder/components/notifier/notifier');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var NotificationErrorMessageHandler = require('builder/editor/layers/notification-error-message-handler');
var basemapProvidersAndCategories = require('builder/data/basemap-providers-and-categories');
var REQUIRED_OPTS = [
'analysisDefinitionNodesCollection',
'editFeatureOverlay',
'layerDefinitionsCollection',
'legendDefinitionsCollection',
'diDashboardHelpers'
];
var LAYER_TYPE_TO_LAYER_CREATE_METHOD = {
'cartodb': 'createCartoDBLayer',
'gmapsbase': 'createGMapsBaseLayer',
'plain': 'createPlainLayer',
'tiled': 'createTileLayer',
'torque': 'createTorqueLayer',
'wms': 'createWMSLayer'
};
var CARTODBJS_TO_CARTODB_ATTRIBUTE_MAPPINGS = {
'layer_name': ['table_name_alias', 'table_name']
};
var BLACKLISTED_LAYER_DEFINITION_ATTRS = {
'all': [ 'letter', 'kind' ],
'Tiled': [ 'category', 'selected', 'highlighted' ],
'CartoDB': [ 'color', 'letter' ],
'torque': [ 'color', 'letter' ]
};
/**
* Only manage **LAYER** actions between Deep-Insights (CARTO.js) and Builder
*
*/
var LayersIntegration = {
track: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._visMap = this._diDashboardHelpers.visMap();
this._layerDefinitionsCollection.each(this._linkLayerErrors, this);
this._layerDefinitionsCollection.on('add', this._onLayerDefinitionAdded, this);
this._layerDefinitionsCollection.on('sync', this._onLayerDefinitionSynced, this);
this._layerDefinitionsCollection.on('change', this._onLayerDefinitionChanged, this);
this._layerDefinitionsCollection.on('remove', this._onLayerDefinitionRemoved, this);
this._layerDefinitionsCollection.on('layerMoved', this._onLayerDefinitionMoved, this);
this._layerDefinitionsCollection.on('baseLayerChanged', this._onBaseLayerChanged, this);
this._layerDefinitionsCollection.each(function (layerDefModel) {
LegendManager.track(layerDefModel);
linkLayerInfowindow(layerDefModel, this._visMap);
linkLayerTooltip(layerDefModel, this._visMap);
if (layerDefModel.has('source')) {
this._resetStylesIfNoneApplied(layerDefModel);
}
}, this);
// In order to sync layer selector and layer visibility
this._diDashboardHelpers.getLayers().on('change:visible', function (layer, visible) {
var layerDefModel = this._layerDefinitionsCollection.findWhere({id: layer.id});
if (layerDefModel) {
if (layerDefModel.get('visible') !== visible) {
layerDefModel.save({visible: visible});
}
}
}, this);
return this;
},
_onLayerDefinitionRemoved: function (m) {
if (!m.isNew()) {
var layer = this._diDashboardHelpers.getLayer(m.id);
layer && layer.remove();
}
},
_onLayerDefinitionMoved: function (m, from, to) {
this._diDashboardHelpers.moveCartoDBLayer(from, to);
},
_createLayer: function (layerDefModel, options) {
options = options || {};
var attrs = JSON.parse(JSON.stringify(layerDefModel.attributes)); // deep clone
attrs = this._adaptAttrsToCDBjs(layerDefModel.get('type'), attrs);
// create the legends for the new layer
var legends = this._legendDefinitionsCollection.findByLayerDefModel(layerDefModel);
if (legends.length > 0) {
attrs.legends = _.map(legends, function (legend) {
return legend.toJSON();
});
}
var createMethodName = LAYER_TYPE_TO_LAYER_CREATE_METHOD[attrs.type.toLowerCase()];
if (!createMethodName) throw new Error('no create method name found for type ' + attrs.type);
if (attrs.source) {
// Make sure the analysis is created first
var nodeDefModel = this._analysisDefinitionNodesCollection.get(attrs.source);
// Dependency with another integration
this.trigger('onLayerCreation', nodeDefModel);
// Set analysis model instead of the string ID source
attrs.source = this._diDashboardHelpers.getAnalysisByNodeId(attrs.source);
}
var layerPosition = this._layerDefinitionsCollection.indexOf(layerDefModel);
this._visMap[createMethodName](attrs, _.extend({
at: layerPosition
}, options));
linkLayerInfowindow(layerDefModel, this._visMap);
linkLayerTooltip(layerDefModel, this._visMap);
LegendManager.track(layerDefModel);
this._linkLayerErrors(layerDefModel);
},
_linkLayerErrors: function (m) {
var layer = this._diDashboardHelpers.getLayer(m.id);
if (layer) {
if (layer.get('error')) {
this._setLayerError(m, layer.get('error'));
}
layer.on('change:error', function (model, cdbError) {
this._setLayerError(m, cdbError);
}, this);
}
},
_setLayerError: function (layerDefinitionModel, cdbError) {
var notification = Notifier.getNotification(layerDefinitionModel.id);
var mainErrorMessage = layerDefinitionModel.getName() + ': ' + (cdbError && cdbError.message);
if (!cdbError) {
layerDefinitionModel.unset('error');
notification && Notifier.removeNotification(notification);
return;
}
var errorMessage = NotificationErrorMessageHandler.extractError(mainErrorMessage);
if (notification) {
notification.update({
status: errorMessage.type,
info: errorMessage.message
});
} else {
Notifier.addNotification({
id: layerDefinitionModel.id,
status: errorMessage.type,
closable: true,
button: false,
info: errorMessage.message
});
}
if (cdbError.subtype === 'turbo-carto') {
var line;
try {
line = cdbError.context.source.start.line;
} catch (error) {}
layerDefinitionModel.set('error', {
type: cdbError.type,
subtype: cdbError.subtype,
line: line,
message: cdbError.message
});
} else if (errorMessage) {
layerDefinitionModel.set('error', {
type: errorMessage.type,
subtype: cdbError.subtype,
message: errorMessage.message
});
}
},
_onLayerDefinitionAdded: function (m, c, opts) {
// Base and labels layers are synced in a separate method
if (!layerTypesAndKinds.isTypeDataLayer(m.get('type'))) {
return;
}
// If added but not yet saved, postpone the creation until persisted (see sync listener)
if (!m.isNew()) {
if (!this._diDashboardHelpers.getLayer(m.id)) {
this._createLayer(m);
} else {
// we need to sync model positions
this._tryUpdateLayerPosition(m);
}
}
},
_tryUpdateLayerPosition: function (m) {
var builderPosition = this._layerDefinitionsCollection.indexOf(m);
var cdbLayer = this._diDashboardHelpers.getLayer(m.id);
var cdbPosition;
if (cdbLayer) {
cdbPosition = this._diDashboardHelpers.getLayers().indexOf(cdbLayer);
}
var indexChanges = m.isDataLayer() && cdbPosition > 0 && builderPosition > 0 && builderPosition !== cdbPosition;
if (indexChanges) {
this._diDashboardHelpers.moveCartoDBLayer(cdbPosition, builderPosition);
}
},
_onLayerDefinitionSynced: function (m) {
// Base and labels layers are synced in a separate method
if (!layerTypesAndKinds.isTypeDataLayer(m.get('type'))) {
return;
}
if (!this._diDashboardHelpers.getLayer(m.id)) {
this._createLayer(m);
}
},
_onLayerDefinitionChanged: function (layerDefinitionModel, changedAttributes) {
var attrs = layerDefinitionModel.changedAttributes();
var attrsNames = _.keys(attrs);
// Base and labels layers are synced in a separate method
if (!layerTypesAndKinds.isTypeDataLayer(layerDefinitionModel.get('type'))) {
return;
}
// return if only the 'error' attribute has changed (no need to sync anything)
if (attrsNames.length === 1 && attrsNames[0] === 'error') {
return;
}
var layer = this._diDashboardHelpers.getLayer(layerDefinitionModel.id);
if (!layerDefinitionModel.isNew()) {
if (!layer) {
this._createLayer(layerDefinitionModel);
return;
}
if (attrs.type) {
layer.remove();
this._createLayer(layerDefinitionModel);
} else {
if (layerDefinitionModel.get('source') && !layer.get('source')) {
attrs.source = layerDefinitionModel.get('source');
}
var onlySourceChanged = attrs.source && _.keys(attrs).length === 1;
if (attrs.source) {
// Set analysis model instead of the string ID source
attrs.source = this._diDashboardHelpers.getAnalysisByNodeId(attrs.source);
// Set source with setSource method
layer.setSource(attrs.source, { silent: !onlySourceChanged });
// Remove source form attrs to avoid updating source
delete attrs.source;
}
attrs = this._adaptAttrsToCDBjs(layerDefinitionModel.get('type'), attrs);
layer.update(attrs, { silent: onlySourceChanged });
}
// Find an animated layer if exists
var animatedLayerDefinitionModel = this._layerDefinitionsCollection.find(function (model) {
return model && model.styleModel && model.styleModel.get('type') === 'animation';
});
// Dependency with widgets-integration
// If there is an animated layer, use that layer, else use the provided layer
this.trigger('onLayerChanged', animatedLayerDefinitionModel || layerDefinitionModel);
}
},
_onBaseLayerChanged: function () {
var baseLayerDefinition = this._layerDefinitionsCollection.getBaseLayer();
var newBaseLayerAttrs = baseLayerDefinition.changedAttributes();
var newBaseLayerType = baseLayerDefinition.get('type');
var newMapProvider = basemapProvidersAndCategories.getProvider(newBaseLayerType);
var mapProviderChanged = false;
if (baseLayerDefinition.hasChanged('type')) {
var previousBaseLayerType = baseLayerDefinition.previous('type');
var previousMapProvider = basemapProvidersAndCategories.getProvider(previousBaseLayerType);
mapProviderChanged = previousMapProvider !== newMapProvider;
}
// If the map provider has changed (eg: Leaflet -> Google Maps), we add/update/remove base and
// labels layers silently so that CartoDB.js doesn't pick up those changes and tries to add/update/remove
// layers until the new map provider has been set
var handleLayersSilently = mapProviderChanged;
// Base layer
var cdbjsLayer = this._diDashboardHelpers.getLayer(baseLayerDefinition.id);
// If the type of base layer has changed. eg: Tiled -> Plain
if (newBaseLayerAttrs.type) {
cdbjsLayer.remove({ silent: handleLayersSilently });
this._createLayer(baseLayerDefinition, { silent: handleLayersSilently });
} else {
cdbjsLayer.update(this._adaptAttrsToCDBjs(baseLayerDefinition.get('type'), newBaseLayerAttrs), {
silent: handleLayersSilently
});
}
// Labels layer
var labelsLayerDefinition = this._layerDefinitionsCollection.getLabelsLayer();
var cdbjsTopLayer = this._diDashboardHelpers.getLayers().last();
var cdbjsHasLabelsLayer = cdbjsTopLayer.get('type') === 'Tiled';
if (labelsLayerDefinition) {
if (cdbjsHasLabelsLayer) {
var changedAttrs = labelsLayerDefinition.changedAttributes();
if (changedAttrs) {
cdbjsTopLayer.update(this._adaptAttrsToCDBjs(labelsLayerDefinition.get('type'), changedAttrs), {
silent: handleLayersSilently
});
}
} else {
this._createLayer(labelsLayerDefinition, { silent: handleLayersSilently });
}
} else if (cdbjsHasLabelsLayer) {
cdbjsTopLayer.remove({ silent: handleLayersSilently });
}
// Map provider
this._visMap.set('provider', newMapProvider);
if (handleLayersSilently) {
// Reload map if everything (previously) was done silently
this._diDashboardHelpers.reloadMap();
}
// Render again the edit-feature-overlay, in order to
// decide if delegate or not events
this._editFeatureOverlay.render();
// Dependency with map-integration class
this.trigger('onBaseLayerChanged');
},
_resetStylesIfNoneApplied: function (layerDefModel) {
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
var nodeModel = this._diDashboardHelpers.getAnalysisByNodeId(layerDefModel.get('source'));
var isAnalysisNode = nodeModel && nodeModel.get('type') !== 'source';
var isDone = nodeModel && nodeModel.isDone();
var queryGeometryModel = nodeDefModel && nodeDefModel.queryGeometryModel;
var styleModel = layerDefModel.styleModel;
if (isAnalysisNode && styleModel.hasNoneStyles() && isDone) {
var simpleGeom = queryGeometryModel.get('simple_geom');
var applyDefaultStyles = function () {
simpleGeom = queryGeometryModel.get('simple_geom');
styleModel.setDefaultPropertiesByType('simple', simpleGeom);
};
if (!simpleGeom) {
queryGeometryModel.once('change:simple_geom', applyDefaultStyles, this);
queryGeometryModel.fetch();
} else {
applyDefaultStyles();
}
}
},
_adaptAttrsToCDBjs: function (layerType, attrs) {
attrs = _.omit(attrs, BLACKLISTED_LAYER_DEFINITION_ATTRS['all'], BLACKLISTED_LAYER_DEFINITION_ATTRS[layerType]);
_.each(CARTODBJS_TO_CARTODB_ATTRIBUTE_MAPPINGS, function (cdbAttrs, cdbjsAttr) {
_.each(cdbAttrs, function (cdbAttr) {
if (attrs[cdbAttr] && !attrs[cdbjsAttr]) {
attrs[cdbjsAttr] = attrs[cdbAttr];
}
});
});
return attrs;
}
};
_.extend(LayersIntegration, Backbone.Events);
module.exports = LayersIntegration;