858 lines
34 KiB
JavaScript
Executable File
858 lines
34 KiB
JavaScript
Executable File
var _ = require('underscore');
|
|
var $ = require('jquery');
|
|
var nodeIds = require('builder/value-objects/analysis-node-ids');
|
|
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
|
|
var MetricsTypes = require('builder/components/metrics/metrics-types');
|
|
|
|
var SimpleStyleDefaults = require('builder/editor/style/style-defaults/simple-style-defaults');
|
|
var camshaftReference = require('./camshaft-reference');
|
|
var layerTypesAndKinds = require('./layer-types-and-kinds');
|
|
var layerColors = require('./layer-colors');
|
|
var Notifier = require('builder/components/notifier/notifier');
|
|
var resetStylePerNode = require('builder/helpers/reset-style-per-node');
|
|
var Router = require('builder/routes/router');
|
|
|
|
var TABLE_ORIGIN = 'table';
|
|
|
|
/**
|
|
* Coordinate side-effects done on explicit interactions.
|
|
* @param {Object} params
|
|
* @param {Object} params.userModel
|
|
* @param {Object} params.analysisDefinitionsCollection
|
|
* @param {Object} params.analysisDefinitionNodesCollection
|
|
* @param {Object} params.widgetDefinitionsCollection
|
|
* @return {Object} that contains all user-actions that the user may do related to a map
|
|
*/
|
|
module.exports = function (params) {
|
|
if (!params.userModel) throw new Error('userModel is required');
|
|
if (!params.analysisDefinitionsCollection) throw new Error('analysisDefinitionsCollection is required');
|
|
if (!params.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
|
|
if (!params.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
|
if (!params.widgetDefinitionsCollection) throw new Error('widgetDefinitionsCollection is required');
|
|
|
|
var userModel = params.userModel;
|
|
var analysisDefinitionsCollection = params.analysisDefinitionsCollection;
|
|
var analysisDefinitionNodesCollection = params.analysisDefinitionNodesCollection;
|
|
var layerDefinitionsCollection = params.layerDefinitionsCollection;
|
|
var widgetDefinitionsCollection = params.widgetDefinitionsCollection;
|
|
|
|
var createDefaultCartoDBAttrs = function (oldLayerStyle) {
|
|
return {
|
|
kind: 'carto',
|
|
options: {
|
|
interactivity: '',
|
|
tile_style: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
|
|
cartocss: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
|
|
style_version: '2.1.1',
|
|
visible: true,
|
|
style_properties: (oldLayerStyle && oldLayerStyle.options.style_properties),
|
|
sql_wrap: (oldLayerStyle && oldLayerStyle.options.sql_wrap)
|
|
},
|
|
tooltip: oldLayerStyle ? oldLayerStyle.tooltip : {},
|
|
infowindow: oldLayerStyle ? oldLayerStyle.infowindow : {}
|
|
};
|
|
};
|
|
|
|
var deleteOrphanedAnalyses = function () {
|
|
analysisDefinitionNodesCollection
|
|
.toArray()
|
|
.forEach(function (nodeDefModel) {
|
|
if (!layerDefinitionsCollection.anyContainsNode(nodeDefModel)) {
|
|
nodeDefModel.destroy();
|
|
}
|
|
});
|
|
|
|
analysisDefinitionsCollection
|
|
.toArray()
|
|
.forEach(function (analysisDefModel) {
|
|
if (!analysisDefModel.getNodeDefinitionModel()) {
|
|
analysisDefModel.destroy();
|
|
}
|
|
});
|
|
};
|
|
|
|
var restoreWidgetsFromLayer = function (affectedWidgets, layerDefModel, callback, callbackOptions) {
|
|
if (!affectedWidgets || !_.isArray(affectedWidgets)) throw new Error('affectedWidgets is required');
|
|
if (!layerDefModel) throw new Error('layerDefModel is required');
|
|
if (!callback) throw new Error('callback is required');
|
|
|
|
var notification;
|
|
var restoringError;
|
|
var onWidgetFinished = _.after(affectedWidgets.length, function () {
|
|
if (notification) {
|
|
if (!restoringError) {
|
|
notification.set({
|
|
status: 'success',
|
|
info: _t('notifications.widgets.restored'),
|
|
closable: true
|
|
});
|
|
} else {
|
|
Notifier.removeNotification(notification);
|
|
}
|
|
}
|
|
|
|
callback(layerDefModel, callbackOptions);
|
|
});
|
|
|
|
if (affectedWidgets.length > 0) {
|
|
notification = Notifier.addNotification({
|
|
status: 'loading',
|
|
info: _t('notifications.widgets.restoring'),
|
|
closable: false
|
|
});
|
|
|
|
// Create widgets with new source and layer_id
|
|
_.each(affectedWidgets, function (attrs) {
|
|
attrs.layer_id = attrs.layer_id || layerDefModel.id;
|
|
|
|
widgetDefinitionsCollection.create(
|
|
attrs, {
|
|
wait: true,
|
|
success: onWidgetFinished,
|
|
error: function (e, mdl) {
|
|
notification = Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.widgets.error.title') +
|
|
_t('notifications.widgets.error.body', {
|
|
body: '',
|
|
error: mdl && mdl.get('title')
|
|
}),
|
|
closable: true
|
|
});
|
|
restoringError = true;
|
|
onWidgetFinished();
|
|
}
|
|
}
|
|
);
|
|
});
|
|
} else {
|
|
callback(layerDefModel, callbackOptions);
|
|
}
|
|
};
|
|
|
|
return {
|
|
_resetStylePerNode: function (nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady) {
|
|
resetStylePerNode(nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady);
|
|
},
|
|
|
|
saveAnalysis: function (analysisFormModel) {
|
|
if (!analysisFormModel.isValid()) return;
|
|
|
|
var nodeDefModel = analysisDefinitionNodesCollection.get(analysisFormModel.id);
|
|
|
|
if (nodeDefModel) {
|
|
analysisFormModel.updateNodeDefinition(nodeDefModel);
|
|
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
|
|
MetricsTracker.track(MetricsTypes.MODIFIED_ANALYSIS, {
|
|
analysis: {
|
|
id: analysisFormModel.attributes['id'],
|
|
natural_id: analysisFormModel.attributes['id'],
|
|
type: analysisFormModel.attributes['type']
|
|
}
|
|
});
|
|
|
|
this._resetStylePerNode(nodeDefModel, layerDefModel, false, true);
|
|
} else {
|
|
nodeDefModel = analysisFormModel.createNodeDefinition(this); // delegate creation to the form, but expect it to call userActions.createAnalysisNode when done with its internal stuff
|
|
}
|
|
nodeDefModel.USER_SAVED = true;
|
|
return nodeDefModel;
|
|
},
|
|
|
|
/**
|
|
* Creates a new analysis node on a particular layer.
|
|
* It's assumed to be created on top of an existing node.
|
|
* @param {Object} nodeAttrs
|
|
* @param {Object} layerDefModel - instance of layer-definition-model
|
|
* @return {Object} instance of analysis-definition-node-model
|
|
*/
|
|
createAnalysisNode: function (nodeAttrs, layerDefModel) {
|
|
var nodeDefModel = analysisDefinitionNodesCollection.add(nodeAttrs, { parse: false });
|
|
|
|
layerDefModel.save({
|
|
cartocss: camshaftReference.getDefaultCartoCSSForType(),
|
|
source: nodeDefModel.id
|
|
});
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
|
|
MetricsTracker.track(MetricsTypes.CREATED_ANALYSIS, {
|
|
analysis: {
|
|
id: nodeDefModel.attributes['id'],
|
|
natural_id: nodeDefModel.attributes['id'],
|
|
type: nodeDefModel.attributes['type']
|
|
}
|
|
});
|
|
|
|
this._resetStylePerNode(nodeDefModel, layerDefModel);
|
|
|
|
return nodeDefModel;
|
|
},
|
|
|
|
saveAnalysisSourceQuery: function (query, nodeDefModel, layerDefModel) {
|
|
if (!nodeDefModel) throw new Error('nodeDefModel is required');
|
|
if (nodeDefModel.get('type') !== 'source') throw new Error('nodeDefModel must be a source node');
|
|
|
|
nodeDefModel.set('query', query || '');
|
|
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
this._resetStylePerNode(nodeDefModel, layerDefModel);
|
|
layerDefModel.save(); // to make sure layer's source ref is persisted
|
|
},
|
|
|
|
saveWidgetOption: function (widgetOptionModel) {
|
|
if (!widgetOptionModel) throw new Error('widgetOptionModel is required');
|
|
|
|
if (widgetOptionModel.analysisDefinitionNodeModel()) { // Might not always have a node-definition, e.g. time-series none-option
|
|
var layerDefModel = widgetOptionModel.layerDefinitionModel();
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
}
|
|
|
|
// delegate back to form model to decide how to save
|
|
return widgetOptionModel.save(widgetDefinitionsCollection);
|
|
},
|
|
|
|
goToEditWidget: function (widgets) {
|
|
if (widgets.length === 1) {
|
|
Router.goToWidget(widgets[0].id);
|
|
}
|
|
},
|
|
|
|
updateWidgetsOrder: function (widgets) {
|
|
return widgetDefinitionsCollection.updateWidgetsOrder(widgets);
|
|
},
|
|
|
|
saveWidget: function (widgetDefModel) {
|
|
if (!widgetDefModel) throw new Error('widgetDefModel is required');
|
|
|
|
var widgetLayerId = widgetDefModel.get('layer_id');
|
|
layerDefinitionsCollection.some(function (layerDefModel) {
|
|
if (layerDefModel.id === widgetLayerId) {
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
return true; // aborts the "some"-iterator
|
|
}
|
|
});
|
|
|
|
widgetDefModel.save();
|
|
},
|
|
|
|
deleteAnalysisNode: function (nodeId) {
|
|
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
|
|
if (!nodeDefModel) return false; // abort if there is no node-definition; nothing to delete/change
|
|
if (!nodeDefModel.canBeDeletedByUser()) return false;
|
|
|
|
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
|
var primarySourceNode = nodeDefModel.getPrimarySource();
|
|
|
|
var containsNode = function (m) {
|
|
return m !== layerDefModel && m !== nodeDefModel && m.containsNode(nodeDefModel);
|
|
};
|
|
_
|
|
.flatten([
|
|
widgetDefinitionsCollection.filter(containsNode),
|
|
layerDefinitionsCollection.filter(containsNode),
|
|
analysisDefinitionsCollection.filter(containsNode),
|
|
analysisDefinitionNodesCollection.filter(containsNode),
|
|
nodeDefModel
|
|
], true)
|
|
.forEach(function (m) {
|
|
m.destroy();
|
|
});
|
|
|
|
// Try to restore old styles if we have them, reset them if not
|
|
var oldNodeStyle = primarySourceNode.getStyleHistoryForLayer(layerDefModel.id);
|
|
if (oldNodeStyle) {
|
|
layerDefModel.set({ source: primarySourceNode.id }, { ignoreSchemaChange: true });
|
|
layerDefModel.styleModel.set(layerDefModel.styleModel.parse(oldNodeStyle.options.style_properties));
|
|
layerDefModel.set({
|
|
cartocss: oldNodeStyle.options.tile_style,
|
|
infowindow: oldNodeStyle.infowindow,
|
|
tooltip: oldNodeStyle.tooltip,
|
|
sql_wrap: oldNodeStyle.options.sql_wrap
|
|
});
|
|
} else {
|
|
layerDefModel.set({ source: primarySourceNode.id });
|
|
}
|
|
layerDefModel.save();
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
|
|
MetricsTracker.track(MetricsTypes.DELETED_ANALYSIS, {
|
|
analysis: {
|
|
id: nodeDefModel.attributes['id'],
|
|
natural_id: nodeDefModel.attributes['id'],
|
|
type: nodeDefModel.attributes['type']
|
|
}
|
|
});
|
|
|
|
this._resetStylePerNode(primarySourceNode, layerDefModel);
|
|
|
|
deleteOrphanedAnalyses();
|
|
},
|
|
|
|
/**
|
|
* Create a new layer for a given table name
|
|
* @param {model} [tableModel]
|
|
* @param {object} [options]
|
|
* @param {number} [options.at]
|
|
*/
|
|
createLayerFromTable: function (tableModel, options) {
|
|
options = options || {};
|
|
|
|
var tableName = tableModel.getUnqualifiedName();
|
|
var ownerName = tableModel.getOwnerName() || userModel.get('username');
|
|
var tableGeometry = tableModel.getGeometryType() && tableModel.getGeometryType()[0];
|
|
|
|
// Setting at assumes there to be at least one non-basemap layer
|
|
var at = options.at || layerDefinitionsCollection.length;
|
|
var layerOnTop = layerDefinitionsCollection.getLayerOnTop();
|
|
var layerOnTopPos = layerDefinitionsCollection.indexOf(layerOnTop);
|
|
var hasTorque = layerDefinitionsCollection.isThereAnyTorqueLayer();
|
|
var hasLabels = layerOnTop &&
|
|
!layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
|
|
!layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));
|
|
|
|
if (hasTorque || hasLabels) {
|
|
layerOnTop.set('order', layerOnTopPos + 1); // persisted on collection.save in success callback below
|
|
|
|
if (hasLabels) { at--; }
|
|
if (hasTorque) { at--; }
|
|
}
|
|
|
|
var newLetter = layerDefinitionsCollection.nextLetter();
|
|
var attrs = createDefaultCartoDBAttrs();
|
|
attrs.letter = newLetter;
|
|
attrs.options.table_name = tableName;
|
|
if (ownerName) {
|
|
attrs.options.user_name = ownerName;
|
|
}
|
|
|
|
if (tableGeometry) {
|
|
attrs.options.style_properties = {
|
|
type: 'simple',
|
|
properties: SimpleStyleDefaults.generateAttributes(tableGeometry)
|
|
};
|
|
}
|
|
|
|
analysisDefinitionNodesCollection.addRelatedTableData(tableModel.toJSON());
|
|
|
|
return layerDefinitionsCollection.create(attrs, {
|
|
wait: true,
|
|
at: at,
|
|
error: options.error,
|
|
origin: TABLE_ORIGIN,
|
|
success: function () {
|
|
layerDefinitionsCollection.each(function (layerDefModel) {
|
|
if (!layerDefModel.isDataLayer()) return;
|
|
if (analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel)) return;
|
|
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
});
|
|
layerDefinitionsCollection.save();
|
|
|
|
var newLayerModel = _.last(layerDefinitionsCollection.models);
|
|
options.success && options.success(newLayerModel);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* A layer for an existing node have different side-effects depending on the context in which the node exists.
|
|
* @param {string} nodeid
|
|
* @param {string} letter of the layer where the node change comes
|
|
* @param {object} cfg
|
|
* @param {number} cfg.at
|
|
*/
|
|
createLayerForAnalysisNode: function (nodeId, fromLayerLetter, cfg) {
|
|
var userMaxLayers = userModel.get('limits').max_layers;
|
|
if (layerDefinitionsCollection.getNumberOfDataLayers() >= userMaxLayers) {
|
|
var err = new Error('max layers reached');
|
|
err.userMaxLayers = userMaxLayers;
|
|
throw err;
|
|
}
|
|
|
|
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
|
|
if (!nodeDefModel) throw new Error('node with id ' + nodeId + ' does not exist');
|
|
if (!cfg) throw new Error('cfg is required');
|
|
if (isNaN(cfg.at) || cfg.at <= 0) throw new Error('cfg.at must be on top of the base layer');
|
|
|
|
var newPosition = cfg.at;
|
|
|
|
var attrs;
|
|
var tableName;
|
|
var tableNameAlias;
|
|
var ownerName;
|
|
var oldNodeStyle;
|
|
var movementType;
|
|
var newLetter = layerDefinitionsCollection.nextLetter();
|
|
var affectedWidgetAttrsBySourceChange = [];
|
|
|
|
var onNewLayerSaved = function (layer, forceResetStyles) {
|
|
this._resetStylePerNode(layer.getAnalysisDefinitionNodeModel(), layer, forceResetStyles);
|
|
layerDefinitionsCollection.save(); // to persist layers order
|
|
MetricsTracker.track(MetricsTypes.DRAGGED_NODE);
|
|
}.bind(this);
|
|
|
|
var prevLayer = layerDefinitionsCollection.findWhere({ source: nodeId, letter: fromLayerLetter });
|
|
if (prevLayer) {
|
|
if (nodeDefModel.hasPrimarySource()) {
|
|
// Node is head of a layer, e.g. given nodeId A3 it should rename prev layer (A => B), and create a new layer (A)
|
|
// where the prev layer was to take over its letter identity and its primary source (A2).
|
|
// The motivation for this is to maintain the layer's state (styles, popup etc.) which really depends on the
|
|
// last analysis output than the layer itself:
|
|
// _______ _______ ______
|
|
// | A | | A | | B | <-- note that B is really A which just got moved & had it's nodes renamed
|
|
// | | | | | |
|
|
// | {A3} | => | | | {B1} |
|
|
// | [A2] | | [A2] | | [A2] |
|
|
// | [A1] | | [A1] | | |
|
|
// | [A0] | | [A0] | | |
|
|
// |______| |______| |______|
|
|
movementType = '1';
|
|
var prevOrder = layerDefinitionsCollection.indexOf(prevLayer);
|
|
var prevLetter = fromLayerLetter;
|
|
|
|
// Change identity of prevLayer (A) so it appears as the new layer (B), including its analysis
|
|
var renamedNodeId = newLetter + '1';
|
|
var renamedNodeDefModel = nodeDefModel.clone(renamedNodeId); // e.g. A3 => B1
|
|
analysisDefinitionNodesCollection.invoke('changeSourceIds', nodeId, renamedNodeId);
|
|
|
|
// We have to manage all widgets associated with the layer due to the operations we do
|
|
// creating a new one and managing the previous one
|
|
var prevLayerNodeIds = _.pluck(prevLayer.ownedPrimaryAnalysisNodes(), 'id');
|
|
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.filter(function (widgetDefModel) {
|
|
return _.contains(prevLayerNodeIds, widgetDefModel.get('source'));
|
|
});
|
|
|
|
_.each(affectedWidgetsBySourceChange, function (widgetDefModel) {
|
|
var attrs;
|
|
var widgetSourceId = widgetDefModel.get('source');
|
|
|
|
// If this widget doesn't point to the "dragged" node, it will
|
|
// keep the source but the layer_id will be totally different
|
|
if (widgetSourceId !== nodeId) {
|
|
attrs = _.extend(
|
|
_.omit(widgetDefModel.toJSON(), 'id', 'layer_id', 'order'),
|
|
{
|
|
avoidNotification: true
|
|
}
|
|
);
|
|
} else {
|
|
// On the other hand, if there is a widget pointing to the dragged node,
|
|
// it will keep the layer_id but the source will be different
|
|
attrs = _.extend(
|
|
_.omit(widgetDefModel.toJSON(), 'id', 'source', 'order'),
|
|
{
|
|
source: {
|
|
id: renamedNodeId
|
|
},
|
|
avoidNotification: true
|
|
}
|
|
);
|
|
}
|
|
|
|
affectedWidgetAttrsBySourceChange.push(attrs);
|
|
widgetDefModel.attributes.avoidNotification = true;
|
|
widgetDefModel.destroy();
|
|
});
|
|
|
|
analysisDefinitionsCollection.newAnalysisForNode(renamedNodeDefModel); // will be saved by saveAnalysisForLayer later since containing that layer's node
|
|
|
|
// Modify all layers using the old node as top source and it doesn't have an analysis definition associated
|
|
layerDefinitionsCollection.each(function (layerDefModel) {
|
|
var containsAnalysisDefinition = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
|
|
if (!containsAnalysisDefinition && layerDefModel.get('source') === nodeId) {
|
|
layerDefModel
|
|
.set('source', renamedNodeId)
|
|
.save();
|
|
}
|
|
});
|
|
|
|
// New layer takes over the identity of the old layer (A), and its primary source as its head node
|
|
// We apply the styles from the new header node (A2) if available
|
|
oldNodeStyle = nodeDefModel.getPrimarySource().getStyleHistoryForLayer(prevLayer.id);
|
|
tableName = prevLayer.get('table_name');
|
|
tableNameAlias = prevLayer.get('table_name_alias');
|
|
ownerName = prevLayer.get('user_name') || userModel.get('username');
|
|
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
|
attrs.options = _.extend({}, attrs.options, {
|
|
sql: 'SELECT * FROM ' + tableName,
|
|
table_name: tableName,
|
|
table_name_alias: tableNameAlias,
|
|
letter: prevLetter,
|
|
color: layerColors.getColorForLetter(prevLetter),
|
|
source: nodeDefModel.getPrimarySource().id
|
|
});
|
|
|
|
if (ownerName) {
|
|
attrs.options.user_name = ownerName;
|
|
}
|
|
|
|
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
|
|
// This is used to keep the style_history valid in both the old a new layer
|
|
attrs.from_layer_id = prevLayer.get('id');
|
|
attrs.from_letter = prevLetter;
|
|
var newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: prevOrder });
|
|
var saveAnalysis = analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel);
|
|
|
|
$.when(saveAnalysis)
|
|
.done(function () {
|
|
// Update the layer after the analyses request
|
|
prevLayer.save({
|
|
letter: newLetter,
|
|
color: layerColors.getColorForLetter(newLetter),
|
|
source: renamedNodeId
|
|
}, { ignoreSchemaChange: true });
|
|
})
|
|
.fail(function () {
|
|
Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.analysis.failed'),
|
|
closable: true
|
|
});
|
|
});
|
|
|
|
// Remove and add prevLayer (A), to move the layers to expected positions
|
|
layerDefinitionsCollection.remove(prevLayer, { silent: true }); // silent to avoid unwanted side-effects; re-added again later
|
|
layerDefinitionsCollection.add(prevLayer, { at: newPosition });
|
|
|
|
// Reset styles from previous layer
|
|
this._resetStylePerNode(prevLayer.getAnalysisDefinitionNodeModel(), prevLayer);
|
|
|
|
newLayerDefModel.save(null, {
|
|
success: function () {
|
|
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
|
|
},
|
|
error: function () {
|
|
Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.layer.error'),
|
|
closable: true
|
|
});
|
|
}
|
|
});
|
|
nodeDefModel.destroy(); // replaced by new node
|
|
} else {
|
|
// Node is the source of a prevLayer
|
|
// Create a new layer which simply points to that source
|
|
// _______ _______ ______
|
|
// | A | | A | | B |
|
|
// | | | | | |
|
|
// | [A0] | => | [A0] | | [A0] |
|
|
// |______| |______| |______|
|
|
|
|
movementType = '2';
|
|
// Keep the same style in the new layer
|
|
oldNodeStyle = {
|
|
options: {
|
|
tile_style: prevLayer.get('cartocss'),
|
|
sql_wrap: prevLayer.get('sql_wrap'),
|
|
style_properties: prevLayer.get('style_properties')
|
|
},
|
|
infowindow: prevLayer.get('infowindow'),
|
|
tooltip: prevLayer.get('tooltip')
|
|
};
|
|
tableName = prevLayer.get('table_name');
|
|
tableNameAlias = prevLayer.get('table_name_alias');
|
|
ownerName = prevLayer.get('user_name') || userModel.get('username');
|
|
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
|
attrs.options = _.extend({}, attrs.options, {
|
|
sql: 'SELECT * FROM ' + tableName,
|
|
table_name: tableName,
|
|
table_name_alias: tableNameAlias,
|
|
letter: newLetter,
|
|
color: layerColors.getColorForLetter(newLetter),
|
|
source: nodeId
|
|
});
|
|
if (ownerName) {
|
|
attrs.options.user_name = ownerName;
|
|
}
|
|
newLayerDefModel = layerDefinitionsCollection.create(attrs, {
|
|
at: newPosition,
|
|
success: function () {
|
|
onNewLayerSaved(newLayerDefModel, false);
|
|
},
|
|
error: function () {
|
|
Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.layer.error'),
|
|
closable: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Node is NOT a head of a node, e.g. given nodeId is 'a2' this would create a new layer B which takes over the
|
|
// ownership of the given node and its underlying nodes
|
|
// _______ _______ ______
|
|
// | A | | A | | B |
|
|
// | | | | | |
|
|
// | [A3] | | [A3] | | {B2} |
|
|
// | {A2} | => | {B2} | | [B1] |
|
|
// | [A1] | | | | [B0] |
|
|
// | [A0] | | | | |
|
|
// |______| |______| |______|
|
|
|
|
movementType = '3';
|
|
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
|
|
|
|
var moveNode = function (oldNode) {
|
|
var newId = nodeIds.changeLetter(oldNode.id, newLetter);
|
|
var newNode = oldNode.clone(newId);
|
|
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.where({ source: oldNode.id });
|
|
|
|
analysisDefinitionNodesCollection.invoke('changeSourceIds', oldNode.id, newId);
|
|
|
|
_.each(affectedWidgetsBySourceChange, function (m) {
|
|
// Store attrs from affected widget for creating a new
|
|
// instance when layer is created
|
|
affectedWidgetAttrsBySourceChange.push(
|
|
_.extend(
|
|
_.omit(m.toJSON(), 'id', 'layer_id', 'source', 'order'),
|
|
{
|
|
avoidNotification: true,
|
|
source: {
|
|
id: newId
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
// Destroy widgets pointing to that source until new layer is created
|
|
m.attributes.avoidNotification = true;
|
|
m.destroy();
|
|
});
|
|
|
|
return newNode;
|
|
};
|
|
var newLayerHeadNode = moveNode(linkedNodesList[0]);
|
|
_.rest(linkedNodesList).forEach(moveNode);
|
|
|
|
// Create the new layer (B). Copy the layer style from the dragged node.
|
|
var ownerLayer = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
|
oldNodeStyle = nodeDefModel.getStyleHistoryForLayer(ownerLayer.id);
|
|
tableName = ownerLayer.get('table_name');
|
|
tableNameAlias = ownerLayer.get('table_name_alias');
|
|
ownerName = ownerLayer.get('user_name') || userModel.get('username');
|
|
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
|
attrs.options = _.extend({}, attrs.options, {
|
|
sql: 'SELECT * FROM ' + tableName,
|
|
table_name: tableName,
|
|
table_name_alias: tableNameAlias,
|
|
letter: newLetter,
|
|
color: layerColors.getColorForLetter(newLetter),
|
|
source: newLayerHeadNode.id
|
|
});
|
|
if (ownerName) {
|
|
attrs.options.user_name = ownerName;
|
|
}
|
|
|
|
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
|
|
// This is used to keep the style_history valid in both the old a new layer
|
|
attrs.from_layer_id = ownerLayer.get('id');
|
|
attrs.from_letter = ownerLayer.get('letter');
|
|
newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: newPosition });
|
|
$.when(analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel))
|
|
.done(function () {
|
|
newLayerDefModel.save()
|
|
.done(function () {
|
|
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
|
|
})
|
|
.fail(function () {
|
|
Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.layer.error'),
|
|
closable: true
|
|
});
|
|
});
|
|
})
|
|
.fail(function () {
|
|
Notifier.addNotification({
|
|
status: 'error',
|
|
info: _t('notifications.analysis.failed'),
|
|
closable: true
|
|
});
|
|
});
|
|
nodeDefModel.destroy(); // replaced by new node
|
|
}
|
|
|
|
deleteOrphanedAnalyses();
|
|
|
|
window.FS && window.FS.log('log', 'Node dragged out case - ' + movementType);
|
|
},
|
|
|
|
moveLayer: function (d) {
|
|
var from = d.from;
|
|
var to = d.to;
|
|
|
|
var movingLayer = layerDefinitionsCollection.at(from);
|
|
layerDefinitionsCollection.remove(movingLayer, { silent: true });
|
|
layerDefinitionsCollection.add(movingLayer, { at: to, parse: false, silent: true });
|
|
|
|
var saveAnalysisPromises = layerDefinitionsCollection
|
|
.chain()
|
|
.map(function (layerDefModel) {
|
|
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
|
|
if (nodeDefModel) {
|
|
return analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
}
|
|
})
|
|
.compact()
|
|
.value();
|
|
|
|
$.when.apply($, saveAnalysisPromises).done(function () { // http://api.jquery.com/jQuery.when/
|
|
layerDefinitionsCollection.save({
|
|
success: function () {
|
|
layerDefinitionsCollection.trigger('layerMoved', movingLayer, from, to);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
deleteLayer: function (id) {
|
|
var layerToDelete = layerDefinitionsCollection.get(id);
|
|
if (!layerToDelete.canBeDeletedByUser()) return;
|
|
|
|
var parentLayer;
|
|
var toDestroy = [];
|
|
|
|
// Iterate over each node in the list, to decide how to remove dependent objects or fold nodes (if possible)
|
|
// under another layer's linked nodes list.
|
|
var linkedNodesList = layerToDelete.ownedPrimaryAnalysisNodes();
|
|
var nodeDefModel;
|
|
|
|
for (var i = 0; i < linkedNodesList.length; i++) {
|
|
nodeDefModel = linkedNodesList[i];
|
|
|
|
parentLayer = layerDefinitionsCollection.findPrimaryParentLayerToAnalysisNode(nodeDefModel, { exclude: toDestroy });
|
|
|
|
// No parent layer? delete all dependent objects, since can't move current nodeDefModel elsewhere
|
|
if (!parentLayer) {
|
|
toDestroy.push(nodeDefModel);
|
|
|
|
layerDefinitionsCollection.each(function (layer) {
|
|
if (layer.containsNode(nodeDefModel)) {
|
|
if (!_.contains(toDestroy, layer)) {
|
|
toDestroy.push(layer);
|
|
}
|
|
|
|
widgetDefinitionsCollection.each(function (widget) {
|
|
if (!_.contains(toDestroy, widget) && widget.containsNode(nodeDefModel)) {
|
|
toDestroy.unshift(widget);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
continue; // to process all sequent nodes in the linekd list, and their (possibly) dependent objects
|
|
}
|
|
|
|
var newParentLinkedNodesList = parentLayer
|
|
.ownedPrimaryAnalysisNodes()
|
|
.concat(nodeDefModel.linkedListBySameLetter());
|
|
|
|
// Since will reassign ids from the start of list need to calculate which sequence id to start on
|
|
var idSequence = newParentLinkedNodesList.length;
|
|
var firstSequence = nodeIds.sequence(newParentLinkedNodesList[0].id);
|
|
|
|
// If first node has a higher sequence start from that one to avoid issues on renaming nodes
|
|
if (idSequence <= firstSequence) {
|
|
idSequence = firstSequence;
|
|
}
|
|
|
|
var lastNode = _.last(newParentLinkedNodesList);
|
|
if (lastNode.get('type') !== 'source') {
|
|
idSequence++; // to start on right id sequence, so last item in newParentLinkedNodesList gets assigned x1 as id, since its source will be a none-source node belong to another layer
|
|
}
|
|
|
|
// Reassign ids one-by-one, from start of list to avoid ids overlapping (e.g. [c2,c1]+[b2,b1] = [c4,c3,c2,c1])
|
|
var prevId = parentLayer.get('letter') + idSequence;
|
|
var moveNodeToParentLayer = function (node) {
|
|
var oldNodeId = node.id;
|
|
var newId = nodeIds.prev(prevId);
|
|
|
|
node.set('id', newId);
|
|
|
|
// Update any depending objects' source
|
|
analysisDefinitionNodesCollection.each(function (m) {
|
|
if (!_.contains(toDestroy, m)) {
|
|
m.changeSourceIds(oldNodeId, newId, true);
|
|
}
|
|
});
|
|
var maybeUpdateSource = function (m) {
|
|
if (m.get('source') === oldNodeId && !_.contains(toDestroy, m)) {
|
|
m.save({ source: newId }, { silent: true });
|
|
}
|
|
};
|
|
layerDefinitionsCollection.each(maybeUpdateSource);
|
|
widgetDefinitionsCollection.each(maybeUpdateSource);
|
|
|
|
prevId = newId;
|
|
|
|
return node;
|
|
};
|
|
var newParentLayerNode = moveNodeToParentLayer(newParentLinkedNodesList[0]);
|
|
_.rest(newParentLinkedNodesList).forEach(moveNodeToParentLayer);
|
|
|
|
parentLayer.save({ source: newParentLayerNode.id });
|
|
|
|
break; // since the remaining nodes have been move to a parent layer
|
|
}
|
|
|
|
if (!_.contains(toDestroy, layerToDelete)) {
|
|
toDestroy.push(layerToDelete);
|
|
}
|
|
|
|
var promise = $.when.apply($, // http://api.jquery.com/jQuery.when/
|
|
_.chain(toDestroy)
|
|
.reduce(function (memo, m) {
|
|
memo.push(m);
|
|
var layerDefModel = layerDefinitionsCollection.get(m.id);
|
|
if (layerDefModel) { // Also delete analyses associated with layers that are being deleted
|
|
var aDefModel = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
|
|
if (aDefModel) {
|
|
memo.push(aDefModel);
|
|
}
|
|
}
|
|
|
|
return memo;
|
|
}, [])
|
|
.unique()
|
|
.map(function (m) {
|
|
m.set({ avoidNotification: true }, { silent: true });
|
|
return m.destroy();
|
|
})
|
|
.value());
|
|
|
|
if (parentLayer) {
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(parentLayer);
|
|
}
|
|
deleteOrphanedAnalyses();
|
|
|
|
return promise;
|
|
},
|
|
|
|
/**
|
|
* E.g. for styles, infowindows etc.
|
|
* @param {object} layerDefModel - layer-definition-model
|
|
* @param {object} options
|
|
*/
|
|
saveLayer: function (layerDefModel, options) {
|
|
if (!layerDefModel) throw new Error('layerDefModel is required');
|
|
|
|
if (layerDefModel.isDataLayer()) {
|
|
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
|
}
|
|
|
|
deleteOrphanedAnalyses();
|
|
|
|
var saveDefaultOptions = {
|
|
shouldPreserveAutoStyle: false
|
|
};
|
|
options = _.extend({}, saveDefaultOptions, options);
|
|
|
|
return layerDefModel.save(null, options);
|
|
}
|
|
};
|
|
};
|