396 lines
14 KiB
JavaScript
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;
|