modules/actions/merge_remote_changes.js (196 lines of code) (raw):

import { utilArrayUnion, utilArrayUniq } from '@id-sdk/util'; import deepEqual from 'fast-deep-equal'; import { diff3Merge } from 'node-diff3'; import { t } from '../core/localizer'; import { actionDeleteMultiple } from './delete_multiple'; import { osmEntity } from '../osm'; export function actionMergeRemoteChanges(id, localGraph, remoteGraph, discardTags, formatUser) { discardTags = discardTags || {}; var _option = 'safe'; // 'safe', 'force_local', 'force_remote' var _conflicts = []; function user(d) { return (typeof formatUser === 'function') ? formatUser(d) : d; } function mergeLocation(remote, target) { function pointEqual(a, b) { var epsilon = 1e-6; return (Math.abs(a[0] - b[0]) < epsilon) && (Math.abs(a[1] - b[1]) < epsilon); } if (_option === 'force_local' || pointEqual(target.loc, remote.loc)) { return target; } if (_option === 'force_remote') { return target.update({loc: remote.loc}); } _conflicts.push(t('merge_remote_changes.conflict.location', { user: user(remote.user) })); return target; } function mergeNodes(base, remote, target) { if (_option === 'force_local' || deepEqual(target.nodes, remote.nodes)) { return target; } if (_option === 'force_remote') { return target.update({nodes: remote.nodes}); } var ccount = _conflicts.length; var o = base.nodes || []; var a = target.nodes || []; var b = remote.nodes || []; var nodes = []; var hunks = diff3Merge(a, o, b, { excludeFalseConflicts: true }); for (var i = 0; i < hunks.length; i++) { var hunk = hunks[i]; if (hunk.ok) { nodes.push.apply(nodes, hunk.ok); } else { // for all conflicts, we can assume c.a !== c.b // because `diff3Merge` called with `true` option to exclude false conflicts.. var c = hunk.conflict; if (deepEqual(c.o, c.a)) { // only changed remotely nodes.push.apply(nodes, c.b); } else if (deepEqual(c.o, c.b)) { // only changed locally nodes.push.apply(nodes, c.a); } else { // changed both locally and remotely _conflicts.push(t('merge_remote_changes.conflict.nodelist', { user: user(remote.user) })); break; } } } return (_conflicts.length === ccount) ? target.update({nodes: nodes}) : target; } function mergeChildren(targetWay, children, updates, graph) { function isUsed(node, targetWay) { var hasInterestingParent = graph.parentWays(node) .some(function(way) { return way.id !== targetWay.id; }); return node.hasInterestingTags() || hasInterestingParent || graph.parentRelations(node).length > 0; } var ccount = _conflicts.length; for (var i = 0; i < children.length; i++) { var id = children[i]; var node = graph.hasEntity(id); // remove unused childNodes.. if (targetWay.nodes.indexOf(id) === -1) { if (node && !isUsed(node, targetWay)) { updates.removeIds.push(id); } continue; } // restore used childNodes.. var local = localGraph.hasEntity(id); var remote = remoteGraph.hasEntity(id); var target; if (_option === 'force_remote' && remote && remote.visible) { updates.replacements.push(remote); } else if (_option === 'force_local' && local) { target = osmEntity(local); if (remote) { target = target.update({ version: remote.version }); } updates.replacements.push(target); } else if (_option === 'safe' && local && remote && local.version !== remote.version) { target = osmEntity(local, { version: remote.version }); if (remote.visible) { target = mergeLocation(remote, target); } else { _conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) })); } if (_conflicts.length !== ccount) break; updates.replacements.push(target); } } return targetWay; } function updateChildren(updates, graph) { for (var i = 0; i < updates.replacements.length; i++) { graph = graph.replace(updates.replacements[i]); } if (updates.removeIds.length) { graph = actionDeleteMultiple(updates.removeIds)(graph); } return graph; } function mergeMembers(remote, target) { if (_option === 'force_local' || deepEqual(target.members, remote.members)) { return target; } if (_option === 'force_remote') { return target.update({members: remote.members}); } _conflicts.push(t('merge_remote_changes.conflict.memberlist', { user: user(remote.user) })); return target; } function mergeTags(base, remote, target) { if (_option === 'force_local' || deepEqual(target.tags, remote.tags)) { return target; } if (_option === 'force_remote') { return target.update({tags: remote.tags}); } var ccount = _conflicts.length; var o = base.tags || {}; var a = target.tags || {}; var b = remote.tags || {}; var keys = utilArrayUnion(utilArrayUnion(Object.keys(o), Object.keys(a)), Object.keys(b)) .filter(function(k) { return !discardTags[k]; }); var tags = Object.assign({}, a); // shallow copy var changed = false; for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (o[k] !== b[k] && a[k] !== b[k]) { // changed remotely.. if (o[k] !== a[k]) { // changed locally.. _conflicts.push(t('merge_remote_changes.conflict.tags', { tag: k, local: a[k], remote: b[k], user: user(remote.user) })); } else { // unchanged locally, accept remote change.. if (b.hasOwnProperty(k)) { tags[k] = b[k]; } else { delete tags[k]; } changed = true; } } } return (changed && _conflicts.length === ccount) ? target.update({tags: tags}) : target; } // `graph.base()` is the common ancestor of the two graphs. // `localGraph` contains user's edits up to saving // `remoteGraph` contains remote edits to modified nodes // `graph` must be a descendent of `localGraph` and may include // some conflict resolution actions performed on it. // // --- ... --- `localGraph` -- ... -- `graph` // / // `graph.base()` --- ... --- `remoteGraph` // var action = function(graph) { var updates = { replacements: [], removeIds: [] }; var base = graph.base().entities[id]; var local = localGraph.entity(id); var remote = remoteGraph.entity(id); var target = osmEntity(local, { version: remote.version }); // delete/undelete if (!remote.visible) { if (_option === 'force_remote') { return actionDeleteMultiple([id])(graph); } else if (_option === 'force_local') { if (target.type === 'way') { target = mergeChildren(target, utilArrayUniq(local.nodes), updates, graph); graph = updateChildren(updates, graph); } return graph.replace(target); } else { _conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) })); return graph; // do nothing } } // merge if (target.type === 'node') { target = mergeLocation(remote, target); } else if (target.type === 'way') { // pull in any child nodes that may not be present locally.. graph.rebase(remoteGraph.childNodes(remote), [graph], false); target = mergeNodes(base, remote, target); target = mergeChildren(target, utilArrayUnion(local.nodes, remote.nodes), updates, graph); } else if (target.type === 'relation') { target = mergeMembers(remote, target); } target = mergeTags(base, remote, target); if (!_conflicts.length) { graph = updateChildren(updates, graph).replace(target); } return graph; }; action.withOption = function(opt) { _option = opt; return action; }; action.conflicts = function() { return _conflicts; }; return action; }