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); } }; };