mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
1. All services are disabled in testing now to prevent network accesses 2. Only services are enabled when needed to test something 3. Many changes throughout code to allow iD to run with services disabled (e.g. check for osm service instead of assuming context.connection() will work) 4. Actually export the services so we can disable and enable them
392 lines
12 KiB
JavaScript
392 lines
12 KiB
JavaScript
import * as d3 from 'd3';
|
|
import _ from 'lodash';
|
|
|
|
import { t } from '../util/locale';
|
|
import { JXON } from '../util/jxon';
|
|
|
|
import {
|
|
actionDiscardTags,
|
|
actionMergeRemoteChanges,
|
|
actionNoop,
|
|
actionRevert
|
|
} from '../actions';
|
|
|
|
import { coreGraph } from '../core';
|
|
import { modeBrowse } from './index';
|
|
|
|
import {
|
|
uiConflicts,
|
|
uiConfirm,
|
|
uiCommit,
|
|
uiLoading,
|
|
uiSuccess
|
|
} from '../ui';
|
|
|
|
import {
|
|
utilDisplayName,
|
|
utilDisplayType
|
|
} from '../util';
|
|
|
|
|
|
|
|
export function modeSave(context) {
|
|
var mode = {
|
|
id: 'save'
|
|
};
|
|
|
|
var commit = uiCommit(context)
|
|
.on('cancel', cancel)
|
|
.on('save', save);
|
|
|
|
|
|
function cancel() {
|
|
context.enter(modeBrowse(context));
|
|
}
|
|
|
|
|
|
function save(changeset, tryAgain) {
|
|
|
|
var osm = context.connection(),
|
|
loading = uiLoading(context).message(t('save.uploading')).blocking(true),
|
|
history = context.history(),
|
|
origChanges = history.changes(actionDiscardTags(history.difference())),
|
|
localGraph = context.graph(),
|
|
remoteGraph = coreGraph(history.base(), true),
|
|
modified = _.filter(history.difference().summary(), {changeType: 'modified'}),
|
|
toCheck = _.map(_.map(modified, 'entity'), 'id'),
|
|
toLoad = withChildNodes(toCheck, localGraph),
|
|
conflicts = [],
|
|
errors = [];
|
|
|
|
if (!osm) return;
|
|
|
|
if (!tryAgain) {
|
|
history.perform(actionNoop()); // checkpoint
|
|
}
|
|
|
|
context.container().call(loading);
|
|
|
|
if (toCheck.length) {
|
|
osm.loadMultiple(toLoad, loaded);
|
|
} else {
|
|
upload();
|
|
}
|
|
|
|
|
|
function withChildNodes(ids, graph) {
|
|
return _.uniq(_.reduce(ids, function(result, id) {
|
|
var entity = graph.entity(id);
|
|
if (entity.type === 'way') {
|
|
try {
|
|
var cn = graph.childNodes(entity);
|
|
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)));
|
|
}
|
|
|
|
|
|
// 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);
|
|
osm.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="' + osm.userURL(d) + '" target="_blank">' + d + '</a>';
|
|
}
|
|
function entityName(entity) {
|
|
return utilDisplayName(entity) || (utilDisplayType(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 = actionMergeRemoteChanges,
|
|
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)
|
|
]
|
|
});
|
|
});
|
|
|
|
upload();
|
|
}
|
|
|
|
|
|
function upload() {
|
|
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(actionDiscardTags(history.difference()));
|
|
if (changes.modified.length || changes.created.length || changes.deleted.length) {
|
|
osm.putChangeset(changeset, changes, uploadCallback);
|
|
} else { // changes were insignificant or reverted by user
|
|
d3.select('.inspector-wrap *').remove();
|
|
loading.close();
|
|
context.flush();
|
|
cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function uploadCallback(err, changeset) {
|
|
if (err) {
|
|
errors.push({
|
|
msg: err.responseText,
|
|
details: [ t('save.status_code', { code: err.status }) ]
|
|
});
|
|
showErrors();
|
|
} else {
|
|
history.clearSaved();
|
|
success(changeset);
|
|
// Add delay to allow for postgres replication #1646 #2678
|
|
window.setTimeout(function() {
|
|
d3.select('.inspector-wrap *').remove();
|
|
loading.close();
|
|
context.flush();
|
|
}, 2500);
|
|
}
|
|
}
|
|
|
|
|
|
function showConflicts() {
|
|
var selection = context.container()
|
|
.select('#sidebar')
|
|
.append('div')
|
|
.attr('class','sidebar-component');
|
|
|
|
loading.close();
|
|
|
|
selection.call(uiConflicts(context)
|
|
.list(conflicts)
|
|
.on('download', function() {
|
|
var data = JXON.stringify(changeset.update({ id: 'CHANGEME' }).osmChangeJXON(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(actionRevert(children[j]));
|
|
}
|
|
}
|
|
history.replace(actionRevert(conflicts[i].id));
|
|
}
|
|
}
|
|
|
|
selection.remove();
|
|
save(changeset, true);
|
|
})
|
|
);
|
|
}
|
|
|
|
|
|
function showErrors() {
|
|
var selection = uiConfirm(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(changeset) {
|
|
commit.reset();
|
|
context.enter(modeBrowse(context)
|
|
.sidebar(uiSuccess(context)
|
|
.changeset(changeset)
|
|
.on('cancel', function() {
|
|
context.ui().sidebar.hide();
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
mode.enter = function() {
|
|
function done() {
|
|
context.ui().sidebar.show(commit);
|
|
}
|
|
|
|
context.container().selectAll('#content')
|
|
.attr('class', 'inactive');
|
|
|
|
var osm = context.connection();
|
|
if (!osm) return;
|
|
|
|
if (osm.authenticated()) {
|
|
done();
|
|
} else {
|
|
osm.authenticate(function(err) {
|
|
if (err) {
|
|
cancel();
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
mode.exit = function() {
|
|
context.container().selectAll('#content')
|
|
.attr('class', 'active');
|
|
|
|
context.ui().sidebar.hide();
|
|
};
|
|
|
|
return mode;
|
|
}
|