mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 09:12:52 +00:00
341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
import { t } from '../util/locale';
|
|
import _ from 'lodash';
|
|
import { Commit, Conflicts, Loading, Success } from '../ui/index';
|
|
import { DiscardTags, MergeRemoteChanges, Noop, Revert } from '../actions/index';
|
|
import { displayName, displayType } from '../util/index';
|
|
import { Browse } from './index';
|
|
import { Graph } from '../core/index';
|
|
import { JXON } from '../util/jxon';
|
|
|
|
export function Save(context) {
|
|
var ui = Commit(context)
|
|
.on('cancel', cancel)
|
|
.on('save', save);
|
|
|
|
function cancel() {
|
|
context.enter(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') {
|
|
try {
|
|
var cn = graph.childNodes(e);
|
|
result.push.apply(result, _.map(_.filter(cn, 'version'), 'id'));
|
|
} catch (err) {
|
|
/* eslint-disable no-console */
|
|
if (typeof console !== 'undefined') console.error(err);
|
|
/* eslint-enable no-console */
|
|
}
|
|
}
|
|
return result;
|
|
}, _.clone(ids)));
|
|
}
|
|
|
|
var loading = Loading(context).message(t('save.uploading')).blocking(true),
|
|
history = context.history(),
|
|
origChanges = history.changes(DiscardTags(history.difference())),
|
|
localGraph = context.graph(),
|
|
remoteGraph = Graph(history.base(), true),
|
|
modified = _.filter(history.difference().summary(), {changeType: 'modified'}),
|
|
toCheck = _.map(_.map(modified, 'entity'), 'id'),
|
|
toLoad = withChildNodes(toCheck, localGraph),
|
|
conflicts = [],
|
|
errors = [];
|
|
|
|
if (!tryAgain) history.perform(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(_.map(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 '<a href="' + context.connection().userURL(d) + '" target="_blank">' + d + '</a>';
|
|
}
|
|
function entityName(entity) {
|
|
return displayName(entity) || (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 = 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(DiscardTags(history.difference()));
|
|
if (changes.modified.length || changes.created.length || changes.deleted.length) {
|
|
context.connection().putChangeset(
|
|
changes,
|
|
context.version,
|
|
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(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(Revert(children[j]));
|
|
}
|
|
}
|
|
history.replace(Revert(conflicts[i].id));
|
|
}
|
|
}
|
|
|
|
selection.remove();
|
|
save(e, true);
|
|
})
|
|
);
|
|
}
|
|
|
|
|
|
function showErrors() {
|
|
var selection = 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(Browse(context)
|
|
.sidebar(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(err) {
|
|
if (err) {
|
|
cancel();
|
|
} else {
|
|
context.ui().sidebar.show(ui);
|
|
}
|
|
});
|
|
};
|
|
|
|
mode.exit = function() {
|
|
context.ui().sidebar.hide();
|
|
};
|
|
|
|
return mode;
|
|
}
|