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) { var loading = iD.ui.Loading(context).message(t('save.uploading')).blocking(true), history = context.history(), origChanges = history.changes(iD.actions.DiscardTags(history.difference())), altGraph = iD.Graph(history.base(), true), modified = _.filter(history.difference().summary(), {changeType: 'modified'}), toCheck = _.pluck(_.pluck(modified, 'entity'), 'id'), deletedIds = [], didMerge = false, conflicts = [], errors = [], confirm; context.container() .call(loading); if (toCheck.length) { // Reload modified entities into an alternate graph and check for conflicts.. _.each(toCheck, loadAndCheck); } else { finalize(); } function loadAndCheck(id) { context.connection().loadEntity(id, function(err, result) { toCheck = _.without(toCheck, id); if (err) { if (err.status === 410) { // Status: Gone (contains no responseText) addDeleteConflict(id, err); } else { errors.push({ id: id, msg: err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); } } else { _.each(result.data, function(entity) { altGraph.replace(entity); }); checkConflicts(id); } if (!toCheck.length) { finalize(); } }); } function addDeleteConflict(id, err) { if (deletedIds.indexOf(id) !== -1) return; else deletedIds.push(id); function undelete(id) { return function(graph) { var entity = context.entity(id), target = iD.Entity(entity, { version: +entity.version + 1 }); return graph.replace(target); }; } var local = context.graph().entity(id); conflicts.push({ id: id, msg: t('save.status_gone', { name: entityName(local) }), details: [ t('save.status_code', { code: err.status }) ], choices: [ choice(id, t('save.conflict.restore'), [ undelete(id), t('save.conflict.annotation.restore', {id: id})]), choice(id, t('save.conflict.delete'), [ iD.actions.DeleteMultiple([id]), t('save.conflict.annotation.delete', {id: id})]) ] }); } function checkConflicts(id) { var graph = context.graph(), local = graph.entity(id), remote = altGraph.entity(id); if (local.version !== remote.version) { var action = iD.actions.MergeRemoteChanges, merge = action(id, graph, altGraph, formatUser), diff = context.perform(merge), details = merge.conflicts(); if (diff.length()) { didMerge = true; } else { var forceLocal = action(id, graph, altGraph, formatUser).withOption('force_local'), forceRemote = action(id, graph, altGraph, formatUser).withOption('force_remote'); conflicts.push({ id: id, msg: t('save.conflict.message', { name: entityName(local) }), details: details, choices: [ choice(id, t('save.conflict.keep_local'), [ forceLocal, t('save.conflict.annotation.keep_local', {id: id})]), choice(id, t('save.conflict.keep_remote'), [ forceRemote, t('save.conflict.annotation.keep_remote', {id: id})]) ] }); } } } function finalize() { if (didMerge) { // set undo checkpoint.. context.perform(iD.actions.Noop(), t('save.conflict.annotation.safe')); } if (conflicts.length) { showConflicts(); } else if (errors.length) { showErrors(); } else { context.connection().putChangeset( history.changes(iD.actions.DiscardTags(history.difference())), 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 { loading.close(); context.flush(); success(e, changeset_id); } }); } } function showConflicts() { confirm = context.container() .select('#sidebar') .append('div') .attr('class','sidebar-component'); loading.close(); var header = confirm.append('div') .attr('class', 'header fillL'); header.append('button') .attr('class', 'fr') .on('click', function() { confirm.remove(); }) .append('span') .attr('class', 'icon close'); header.append('h3') .text(t('save.conflict.header')); var body = confirm.append('div') .attr('class', 'body fillL'); body.append('div') .attr('class', 'conflicts-help') .text(t('save.conflict.help')) .append('a') .attr('class', 'conflicts-download') .on('click.download', function() { var data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', origChanges)), win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank'); win.focus(); }) .text(t('save.conflict.download_changes')); body.append('div') .attr('class','message-text conflicts-message-text'); addConflictItems(confirm, conflicts); var buttons = body .append('div') .attr('class','buttons col12 joined conflicts-buttons'); buttons .append('button') .attr('disabled', true) .attr('class', 'action conflicts-button col6') .on('click.try_again', function() { confirm.remove(); save(e); }) .text(t('save.title')); buttons .append('button') .attr('class', 'secondary-action conflicts-button col6') .on('click.cancel', function() { confirm.remove(); }) .text(t('confirm.cancel')); } function addConflictItems(confirm, data) { var message = confirm .select('.message-text'); var items = message .selectAll('.conflict-container') .data(data); var enter = items.enter() .append('div') .attr('class', 'conflict-container') .classed('expanded', function(d, i) { return i === 0; }) .each(function(d,i) { if (i === 0) zoomToEntity(d); }); enter .append('h4') .style('display', function(d, i) { return (i === 0) ? 'block': 'none'; }) .text(function(d, i) { return t('save.conflict.count', { num: i+1, total: data.length }); }); enter .append('a') .attr('class', 'conflict-description') .attr('href', '#') .text(function(d) { return d.msg || t('save.unknown_error_details'); }) .on('click', function(d) { toggleExpanded(this.parentElement, d); d3.event.preventDefault(); }); var details = enter .append('div') .attr('class', 'conflict-detail-container') .style('display', function(d,i) { return i === 0 ? 'block' : 'none'; }); details .append('ul') .attr('class', 'conflict-detail-list') .selectAll('li') .data(function(d) { return d.details || []; }) .enter() .append('li') .attr('class', 'conflict-detail-item') .html(function(d) { return d; }); details .append('div') .attr('class', 'conflict-choice-buttons joined cf') .selectAll('button') .data(function(d) { return d.choices || []; }) .enter() .append('button') .attr('class', 'conflict-choice-button action col6') .text(function(d) { return d.text; }) .on('click', function(d) { d.action(); var container = this.parentElement.parentElement.parentElement; var next = container.parentElement.firstElementChild.classList.contains('expanded') ? container.nextElementSibling : container.parentElement.firstElementChild; window.setTimeout(function() { if (next) { toggleExpanded(next, d); } else { d3.select(container.parentElement).append('div') .attr('class','conflicts-done') .text(t('save.conflict.done')); d3.select('.conflicts-button') .attr('disabled', null); } }, 250); d3.select(container) .transition() .style('opacity', 0) .remove(); d3.event.preventDefault(); }); items.exit() .remove(); function toggleExpanded(el, d) { var error = d3.select(el), detail = d3.select(el.getElementsByTagName('div')[0]), count = d3.select(el.getElementsByTagName('h4')[0]), exp = error.classed('expanded'); // Clear old expanded enter.classed('expanded', false); details.style('display', 'none'); // Set new detail .style('opacity', exp ? 1 : 0) .transition() .style('opacity', exp ? 0 : 1) .style('display', exp ? 'none' : 'block'); count .style('opacity', exp ? 1 : 0) .transition() .style('opacity', exp ? 0 : 1) .style('display', exp ? 'none' : 'block'); zoomToEntity(d); error.classed('expanded', !exp); } } function showErrors() { confirm = iD.ui.confirm(context.container()); loading.close(); confirm .select('.modal-section.header') .append('h3') .text(t('save.error')); addErrorItems(confirm, errors); confirm.okButton(); } function addErrorItems(confirm, data) { var message = confirm .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 formatUser(d) { return '' + d + ''; } function entityName(entity) { return iD.util.displayName(entity) || (iD.util.displayType(entity.id) + ' ' + entity.id); } function choice(id, text, actions) { return { id: id, text: text, action: function() { context.perform.apply(this, actions); } }; } function zoomToEntity(d) { var entity = context.graph().entity(d.id); if (entity) { context.map().zoomTo(entity); context.surface().selectAll( iD.util.entityOrMemberSelector([entity.id], context.graph())) .classed('hover', true); } } 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(ui) { context.ui().sidebar.hide(ui); }))); } var mode = { id: 'save' }; var behaviors = [ iD.behavior.Hover(context), iD.behavior.Select(context), iD.behavior.Lasso(context), iD.modes.DragNode(context).behavior]; mode.enter = function() { behaviors.forEach(function(behavior) { context.install(behavior); }); context.connection().authenticate(function() { context.ui().sidebar.show(ui); }); }; mode.exit = function() { behaviors.forEach(function(behavior) { context.uninstall(behavior); }); context.ui().sidebar.hide(ui); }; return mode; };