modules/core/uploader.js (288 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { utilArrayUnion, utilArrayUniq } from '@id-sdk/util';
import { fileFetcher } from './file_fetcher';
import { actionDiscardTags } from '../actions/discard_tags';
import { actionMergeRemoteChanges } from '../actions/merge_remote_changes';
import { actionNoop } from '../actions/noop';
import { actionRevert } from '../actions/revert';
import { coreGraph } from '../core/graph';
import { t } from '../core/localizer';
import { utilDisplayName, utilDisplayType, utilRebind } from '../util';
export function coreUploader(context) {
var dispatch = d3_dispatch(
// Start and end events are dispatched exactly once each per legitimate outside call to `save`
'saveStarted', // dispatched as soon as a call to `save` has been deemed legitimate
'saveEnded', // dispatched after the result event has been dispatched
'willAttemptUpload', // dispatched before the actual upload call occurs, if it will
'progressChanged',
// Each save results in one of these outcomes:
'resultNoChanges', // upload wasn't attempted since there were no edits
'resultErrors', // upload failed due to errors
'resultConflicts', // upload failed due to data conflicts
'resultSuccess' // upload completed without errors
);
var _isSaving = false;
var _conflicts = [];
var _errors = [];
var _origChanges;
var _discardTags = {};
fileFetcher.get('discarded')
.then(function(d) { _discardTags = d; })
.catch(function() { /* ignore */ });
var uploader = utilRebind({}, dispatch, 'on');
uploader.isSaving = function() {
return _isSaving;
};
uploader.save = function(changeset, tryAgain, checkConflicts) {
// Guard against accidentally entering save code twice - #4641
if (_isSaving && !tryAgain) {
return;
}
var osm = context.connection();
if (!osm) return;
// If user somehow got logged out mid-save, try to reauthenticate..
// This can happen if they were logged in from before, but the tokens are no longer valid.
if (!osm.authenticated()) {
osm.authenticate(function(err) {
if (!err) {
uploader.save(changeset, tryAgain, checkConflicts); // continue where we left off..
}
});
return;
}
if (!_isSaving) {
_isSaving = true;
dispatch.call('saveStarted', this);
}
var history = context.history();
_conflicts = [];
_errors = [];
// Store original changes, in case user wants to download them as an .osc file
_origChanges = history.changes(actionDiscardTags(history.difference(), _discardTags));
// First time, `history.perform` a no-op action.
// Any conflict resolutions will be done as `history.replace`
// Remember to pop this later if needed
if (!tryAgain) {
history.perform(actionNoop());
}
// Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true`
if (!checkConflicts) {
upload(changeset);
// Do the full (slow) conflict check..
} else {
performFullConflictCheck(changeset);
}
};
function performFullConflictCheck(changeset) {
var osm = context.connection();
if (!osm) return;
var history = context.history();
var localGraph = context.graph();
var remoteGraph = coreGraph(history.base(), true);
var summary = history.difference().summary();
var _toCheck = [];
for (var i = 0; i < summary.length; i++) {
var item = summary[i];
if (item.changeType === 'modified') {
_toCheck.push(item.entity.id);
}
}
var _toLoad = withChildNodes(_toCheck, localGraph);
var _loaded = {};
var _toLoadCount = 0;
var _toLoadTotal = _toLoad.length;
if (_toCheck.length) {
dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);
_toLoad.forEach(function(id) { _loaded[id] = false; });
osm.loadMultiple(_toLoad, loaded);
} else {
upload(changeset);
}
return;
function withChildNodes(ids, graph) {
var s = new Set(ids);
ids.forEach(function(id) {
var entity = graph.entity(id);
if (entity.type !== 'way') return;
graph.childNodes(entity).forEach(function(child) {
if (child.version !== undefined) {
s.add(child.id);
}
});
});
return Array.from(s);
}
// 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.message || err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
didResultInErrors();
} else {
var loadMore = [];
result.data.forEach(function(entity) {
remoteGraph.replace(entity);
_loaded[entity.id] = true;
_toLoad = _toLoad.filter(function(val) { return val !== entity.id; });
if (!entity.visible) return;
// Because loadMultiple doesn't download /full like loadEntity,
// need to also load children that aren't already being checked..
var i, id;
if (entity.type === 'way') {
for (i = 0; i < entity.nodes.length; i++) {
id = entity.nodes[i];
if (_loaded[id] === undefined) {
_loaded[id] = false;
loadMore.push(id);
}
}
} else if (entity.type === 'relation' && entity.isMultipolygon()) {
for (i = 0; i < entity.members.length; i++) {
id = entity.members[i].id;
if (_loaded[id] === undefined) {
_loaded[id] = false;
loadMore.push(id);
}
}
}
});
_toLoadCount += result.data.length;
_toLoadTotal += loadMore.length;
dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);
if (loadMore.length) {
_toLoad.push.apply(_toLoad, loadMore);
osm.loadMultiple(loadMore, loaded);
}
if (!_toLoad.length) {
detectConflicts();
upload(changeset);
}
}
}
function detectConflicts() {
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 sameVersions(local, remote) {
if (local.version !== remote.version) return false;
if (local.type === 'way') {
var children = utilArrayUnion(local.nodes, remote.nodes);
for (var i = 0; i < children.length; i++) {
var a = localGraph.hasEntity(children[i]);
var b = remoteGraph.hasEntity(children[i]);
if (a && b && a.version !== b.version) return false;
}
}
return true;
}
_toCheck.forEach(function(id) {
var local = localGraph.entity(id);
var remote = remoteGraph.entity(id);
if (sameVersions(local, remote)) return;
var merge = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags, formatUser);
history.replace(merge);
var mergeConflicts = merge.conflicts();
if (!mergeConflicts.length) return; // merged safely
var forceLocal = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_local');
var forceRemote = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_remote');
var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore'));
var 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)
]
});
});
}
}
function upload(changeset) {
var osm = context.connection();
if (!osm) {
_errors.push({ msg: 'No OSM Service' });
}
if (_conflicts.length) {
didResultInConflicts(changeset);
} else if (_errors.length) {
didResultInErrors();
} else {
var history = context.history();
var changes = history.changes(actionDiscardTags(history.difference(), _discardTags));
if (changes.modified.length || changes.created.length || changes.deleted.length) {
dispatch.call('willAttemptUpload', this);
osm.putChangeset(changeset, changes, uploadCallback);
} else {
// changes were insignificant or reverted by user
didResultInNoChanges();
}
}
}
function uploadCallback(err, changeset) {
if (err) {
if (err.status === 409) { // 409 Conflict
uploader.save(changeset, true, true); // tryAgain = true, checkConflicts = true
} else {
_errors.push({
msg: err.message || err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
didResultInErrors();
}
} else {
didResultInSuccess(changeset);
}
}
function didResultInNoChanges() {
dispatch.call('resultNoChanges', this);
endSave();
context.flush(); // reset iD
}
function didResultInErrors() {
context.history().pop();
dispatch.call('resultErrors', this, _errors);
endSave();
}
function didResultInConflicts(changeset) {
_conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); });
dispatch.call('resultConflicts', this, changeset, _conflicts, _origChanges);
endSave();
}
function didResultInSuccess(changeset) {
// delete the edit stack cached to local storage
context.history().clearSaved();
dispatch.call('resultSuccess', this, changeset);
// Add delay to allow for postgres replication #1646 #2678
window.setTimeout(function() {
endSave();
context.flush(); // reset iD
}, 2500);
}
function endSave() {
_isSaving = false;
dispatch.call('saveEnded', this);
}
uploader.cancelConflictResolution = function() {
context.history().pop();
};
uploader.processResolvedConflicts = function(changeset) {
var history = context.history();
for (var i = 0; i < _conflicts.length; i++) {
if (_conflicts[i].chosen === 1) { // user chose "use theirs"
var entity = context.hasEntity(_conflicts[i].id);
if (entity && entity.type === 'way') {
var children = utilArrayUniq(entity.nodes);
for (var j = 0; j < children.length; j++) {
history.replace(actionRevert(children[j]));
}
}
history.replace(actionRevert(_conflicts[i].id));
}
}
uploader.save(changeset, true, false); // tryAgain = true, checkConflicts = false
};
uploader.reset = function() {
};
return uploader;
}