modules/modes/save.js (375 lines of code) (raw):

import { event as d3_event, select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; 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 { modeBrowse } from './browse'; import { uiConflicts } from '../ui/conflicts'; import { uiConfirm } from '../ui/confirm'; import { uiLoading } from '../ui/loading'; import { utilArrayUnion, utilArrayUniq, utilDisplayName, utilDisplayType, utilKeybinding } from '../util'; var _isSaving = false; export function modeSave(context) { var mode = { id: 'save' }; var keybinding = utilKeybinding('modeSave'); var loading = uiLoading(context) .message(t('save.uploading')) .blocking(true); var _toCheck = []; var _toLoad = []; var _loaded = {}; var _toLoadCount = 0; var _toLoadTotal = 0; var _conflicts = []; var _errors = []; var _origChanges; function cancel() { context.enter(modeBrowse(context)); } mode.save = function(changeset, tryAgain, checkConflicts) { // Guard against accidentally entering save code twice - #4641 if (_isSaving && !tryAgain) { return; } var osm = context.connection(); if (!osm) { cancel(); 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) { cancel(); // quit save mode.. } else { mode.save(changeset, tryAgain, checkConflicts); // continue where we left off.. } }); return; } if (!_isSaving) { keybindingOff(); context.container().call(loading); // block input _isSaving = true; } var history = context.history(); var localGraph = context.graph(); var remoteGraph = coreGraph(history.base(), true); _conflicts = []; _errors = []; // Store original changes, in case user wants to download them as an .osc file _origChanges = history.changes(actionDiscardTags(history.difference())); // First time, `history.perform` a no-op action. // Any conflict resolutions will be done as `history.replace` 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 { var summary = history.difference().summary(); _toCheck = []; for (var i = 0; i < summary.length; i++) { var item = summary[i]; if (item.changeType === 'modified') { _toCheck.push(item.entity.id); } } _toLoad = withChildNodes(_toCheck, localGraph); _loaded = {}; _toLoadCount = 0; _toLoadTotal = _toLoad.length; if (_toCheck.length) { showProgress(_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 }) ] }); showErrors(); } 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; showProgress(_toLoadCount, _toLoadTotal); if (loadMore.length) { _toLoad.push.apply(_toLoad, loadMore); osm.loadMultiple(loadMore, loaded); } if (!_toLoad.length) { detectConflicts(); } } } 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 action = actionMergeRemoteChanges; var 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'); var forceRemote = action(id, localGraph, remoteGraph).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) ] }); }); upload(changeset); } }; function upload(changeset) { var osm = context.connection(); if (!osm) { _errors.push({ msg: 'No OSM Service' }); } if (_conflicts.length) { _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); }); showConflicts(changeset); } else if (_errors.length) { showErrors(); } else { var history = context.history(); 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(); _isSaving = false; context.flush(); cancel(); } } } function uploadCallback(err, changeset) { if (err) { if (err.status === 409) { // 409 Conflict mode.save(changeset, true, true); // tryAgain = true, checkConflicts = true } else { _errors.push({ msg: err.message || err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); } } else { var changeCount = context.history().difference().summary().length; context.history().clearSaved(); context.enter(modeBrowse(context)); context.ui().assistant.didSaveChangset(changeset, changeCount); // Add delay to allow for postgres replication #1646 #2678 window.setTimeout(function() { d3_select('.inspector-wrap *').remove(); loading.close(); _isSaving = false; context.flush(); }, 2500); } } function showProgress(num, total) { var modal = context.container().select('.loading-modal .modal-section'); var progress = modal.selectAll('.progress') .data([0]); // enter/update progress.enter() .append('div') .attr('class', 'progress') .merge(progress) .text(t('save.conflict_progress', { num: num, total: total })); } function showConflicts(changeset) { var history = context.history(); var selection = context.container() .select('.assistant .assistant-body') .append('div') .attr('class','inspector-body'); loading.close(); _isSaving = false; var ui = uiConflicts(context) .conflictList(_conflicts) .origChanges(_origChanges) .on('cancel', function() { history.pop(); selection.remove(); keybindingOn(); }) .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 = utilArrayUniq(entity.nodes); for (var j = 0; j < children.length; j++) { history.replace(actionRevert(children[j])); } } history.replace(actionRevert(_conflicts[i].id)); } } selection.remove(); mode.save(changeset, true, false); // tryAgain = true, checkConflicts = false }); selection.call(ui); } function showErrors() { keybindingOn(); context.history().pop(); loading.close(); _isSaving = false; var selection = uiConfirm(context.container()); 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() { d3_event.preventDefault(); var error = d3_select(this); var detail = d3_select(this.nextElementSibling); var exp = error.classed('expanded'); detail.style('display', exp ? 'none' : 'block'); error.classed('expanded', !exp); }); 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 keybindingOn() { d3_select(document) .call(keybinding.on('⎋', cancel, true)); } function keybindingOff() { d3_select(document) .call(keybinding.unbind); } mode.enter = function() { // make sure the save UI is initially visible context.storage('assistant.collapsed.save', null); keybindingOn(); context.container().selectAll('#content') .attr('class', 'inactive'); var osm = context.connection(); if (!osm) { cancel(); return; } if (!osm.authenticated()) { osm.authenticate(function(err) { if (err) { cancel(); } else { // reload context.enter(mode); } }); } }; mode.exit = function() { _isSaving = false; keybindingOff(); context.container().selectAll('#content') .attr('class', 'active'); }; return mode; }