iD.modes.Save = function(context) { var ui = iD.ui.Commit(context) .on('cancel', cancel) .on('save', save); function cancel() { context.enter(iD.modes.Browse(context)); } function save(e, tryAgain) { function withChildNodes(ids, graph) { return _.uniq(_.reduce(ids, function(result, id) { var e = graph.entity(id); if (e.type === 'way') { var cn = graph.childNodes(e); result.push.apply(result, _.pluck(_.filter(cn, 'version'), 'id')); } return result; }, _.clone(ids))); } var loading = iD.ui.Loading(context).message(t('save.uploading')).blocking(true), history = context.history(), origChanges = history.changes(iD.actions.DiscardTags(history.difference())), localGraph = context.graph(), remoteGraph = iD.Graph(history.base(), true), modified = _.filter(history.difference().summary(), {changeType: 'modified'}), toCheck = _.pluck(_.pluck(modified, 'entity'), 'id'), toLoad = withChildNodes(toCheck, localGraph), conflicts = [], errors = []; if (!tryAgain) history.perform(iD.actions.Noop()); // checkpoint context.container().call(loading); if (toCheck.length) { context.connection().loadMultiple(toLoad, loaded); } else { finalize(); } // Reload modified entities into an alternate graph and check for conflicts.. function loaded(err, result) { if (errors.length) return; if (err) { errors.push({ msg: err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); } else { var loadMore = []; _.each(result.data, function(entity) { remoteGraph.replace(entity); toLoad = _.without(toLoad, entity.id); // Because loadMultiple doesn't download /full like loadEntity, // need to also load children that aren't already being checked.. if (!entity.visible) return; if (entity.type === 'way') { loadMore.push.apply(loadMore, _.difference(entity.nodes, toCheck, toLoad, loadMore)); } else if (entity.type === 'relation' && entity.isMultipolygon()) { loadMore.push.apply(loadMore, _.difference(_.pluck(entity.members, 'id'), toCheck, toLoad, loadMore)); } }); if (loadMore.length) { toLoad.push.apply(toLoad, loadMore); context.connection().loadMultiple(loadMore, loaded); } if (!toLoad.length) { checkConflicts(); } } } function checkConflicts() { function choice(id, text, action) { return { id: id, text: text, action: function() { history.replace(action); } }; } function formatUser(d) { return '' + d + ''; } function entityName(entity) { return iD.util.displayName(entity) || (iD.util.displayType(entity.id) + ' ' + entity.id); } function compareVersions(local, remote) { if (local.version !== remote.version) return false; if (local.type === 'way') { var children = _.union(local.nodes, remote.nodes); for (var i = 0; i < children.length; i++) { var a = localGraph.hasEntity(children[i]), b = remoteGraph.hasEntity(children[i]); if (a && b && a.version !== b.version) return false; } } return true; } _.each(toCheck, function(id) { var local = localGraph.entity(id), remote = remoteGraph.entity(id); if (compareVersions(local, remote)) return; var action = iD.actions.MergeRemoteChanges, merge = action(id, localGraph, remoteGraph, formatUser); history.replace(merge); var mergeConflicts = merge.conflicts(); if (!mergeConflicts.length) return; // merged safely var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'), forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'), keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')), keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete')); conflicts.push({ id: id, name: entityName(local), details: mergeConflicts, chosen: 1, choices: [ choice(id, keepMine, forceLocal), choice(id, keepTheirs, forceRemote) ] }); }); finalize(); } function finalize() { if (conflicts.length) { conflicts.sort(function(a,b) { return b.id.localeCompare(a.id); }); showConflicts(); } else if (errors.length) { showErrors(); } else { var changes = history.changes(iD.actions.DiscardTags(history.difference())); if (changes.modified.length || changes.created.length || changes.deleted.length) { context.connection().putChangeset( changes, e.comment, history.imageryUsed(), function(err, changeset_id) { if (err) { errors.push({ msg: err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); } else { history.clearSaved(); success(e, changeset_id); // Add delay to allow for postgres replication #1646 #2678 window.setTimeout(function() { loading.close(); context.flush(); }, 2500); } }); } else { // changes were insignificant or reverted by user loading.close(); context.flush(); cancel(); } } } function showConflicts() { var selection = context.container() .select('#sidebar') .append('div') .attr('class','sidebar-component'); loading.close(); selection.call(iD.ui.Conflicts(context) .list(conflicts) .on('download', function() { var data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', origChanges)), win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank'); win.focus(); }) .on('cancel', function() { history.pop(); selection.remove(); }) .on('save', function() { for (var i = 0; i < conflicts.length; i++) { if (conflicts[i].chosen === 1) { // user chose "keep theirs" var entity = context.hasEntity(conflicts[i].id); if (entity && entity.type === 'way') { var children = _.uniq(entity.nodes); for (var j = 0; j < children.length; j++) { history.replace(iD.actions.Revert(children[j])); } } history.replace(iD.actions.Revert(conflicts[i].id)); } } selection.remove(); save(e, true); }) ); } function showErrors() { var selection = iD.ui.confirm(context.container()); history.pop(); loading.close(); selection .select('.modal-section.header') .append('h3') .text(t('save.error')); addErrors(selection, errors); selection.okButton(); } function addErrors(selection, data) { var message = selection .select('.modal-section.message-text'); var items = message .selectAll('.error-container') .data(data); var enter = items.enter() .append('div') .attr('class', 'error-container'); enter .append('a') .attr('class', 'error-description') .attr('href', '#') .classed('hide-toggle', true) .text(function(d) { return d.msg || t('save.unknown_error_details'); }) .on('click', function() { var error = d3.select(this), detail = d3.select(this.nextElementSibling), exp = error.classed('expanded'); detail.style('display', exp ? 'none' : 'block'); error.classed('expanded', !exp); d3.event.preventDefault(); }); var details = enter .append('div') .attr('class', 'error-detail-container') .style('display', 'none'); details .append('ul') .attr('class', 'error-detail-list') .selectAll('li') .data(function(d) { return d.details || []; }) .enter() .append('li') .attr('class', 'error-detail-item') .text(function(d) { return d; }); items.exit() .remove(); } } function success(e, changeset_id) { context.enter(iD.modes.Browse(context) .sidebar(iD.ui.Success(context) .changeset({ id: changeset_id, comment: e.comment }) .on('cancel', function() { context.ui().sidebar.hide(); }))); } var mode = { id: 'save' }; mode.enter = function() { context.connection().authenticate(function() { context.ui().sidebar.show(ui); }); }; mode.exit = function() { context.ui().sidebar.hide(); }; return mode; };