Subject: [PATCH] #23555: Replace geometry update
---
Index: core/src/org/openstreetmap/josm/actions/MergeNodesAction.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/actions/MergeNodesAction.java b/core/src/org/openstreetmap/josm/actions/MergeNodesAction.java
--- a/core/src/org/openstreetmap/josm/actions/MergeNodesAction.java	(revision 19018)
+++ b/core/src/org/openstreetmap/josm/actions/MergeNodesAction.java	(date 1711050996635)
@@ -293,6 +293,20 @@
      * @since 12689
      */
     public static Command mergeNodes(Collection<Node> nodes, Node targetLocationNode) {
+        return mergeNodes(nodes, targetLocationNode, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
+
+    /**
+     * Merges the nodes in {@code nodes} at the specified node's location.
+     *
+     * @param nodes the collection of nodes. Ignored if null.
+     * @param targetLocationNode this node's location will be used for the targetNode.
+     * @param strategy The strategy to use for tag conflicts
+     * @return The command necessary to run in order to perform action, or {@code null} if there is nothing to do
+     * @throws IllegalArgumentException if {@code layer} is null
+     * @since xxx
+     */
+    public static Command mergeNodes(Collection<Node> nodes, Node targetLocationNode, CombinePrimitiveResolverDialog.Strategy strategy) {
         if (nodes == null) {
             return null;
         }
@@ -302,7 +316,7 @@
         if (targetNode == null) {
             return null;
         }
-        return mergeNodes(nodes, targetNode, targetLocationNode);
+        return mergeNodes(nodes, targetNode, targetLocationNode, strategy);
     }
 
     /**
@@ -315,6 +329,22 @@
      * @throws IllegalArgumentException if layer is null
      */
     public static Command mergeNodes(Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
+        return mergeNodes(nodes, targetNode, targetLocationNode, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
+
+    /**
+     * Merges the nodes in <code>nodes</code> onto one of the nodes.
+     *
+     * @param nodes the collection of nodes. Ignored if null.
+     * @param targetNode the target node the collection of nodes is merged to. Must not be null.
+     * @param targetLocationNode this node's location will be used for the targetNode.
+     * @param strategy The strategy to use when there are tag conflicts
+     * @return The command necessary to run in order to perform action, or {@code null} if there is nothing to do
+     * @throws IllegalArgumentException if layer is null
+     * @since xxx
+     */
+    public static Command mergeNodes(Collection<Node> nodes, Node targetNode, Node targetLocationNode,
+                                     CombinePrimitiveResolverDialog.Strategy strategy) {
         CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
         if (nodes == null) {
             return null;
@@ -346,7 +376,7 @@
                     cmds.add(new ChangeCommand(targetNode, newTargetNode));
                 }
             }
-            cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(nodeTags, nodes, Collections.singleton(targetNode)));
+            cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(nodeTags, nodes, Collections.singleton(targetNode), strategy));
             if (!nodesToDelete.isEmpty()) {
                 cmds.add(new DeleteCommand(nodesToDelete));
             }
Index: core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java b/core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java
--- a/core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java	(revision 19018)
+++ b/core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java	(date 1711053248450)
@@ -84,6 +84,22 @@
  */
 public class CombinePrimitiveResolverDialog extends JDialog {
 
+    /**
+     * The strategy to use when combining tags
+     */
+    public enum Strategy {
+        /** Keep the tags from the target when there are conflicts */
+        KEEP_TARGET,
+        /** Keep the tags from the source when there are conflicts */
+        KEEP_SOURCE,
+        /** Keep the tags from both the source and the target when there are conflicts */
+        KEEP_BOTH,
+        /** Keep all non-conflicting tags */
+        KEEP_NON_CONFLICTING,
+        /** Ask if there are any conflicts, including when one objects has tags the other object does not */
+        ASK
+    }
+
     private AutoAdjustingSplitPane spTagConflictTypes;
     private final TagConflictResolverModel modelTagConflictResolver;
     protected TagConflictResolver pnlTagConflictResolver;
@@ -486,7 +502,30 @@
             final TagCollection tagsOfPrimitives,
             final Collection<? extends OsmPrimitive> primitives,
             final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException {
+        return launchIfNecessary(tagsOfPrimitives, primitives, targetPrimitives, Strategy.ASK);
+    }
 
+    /**
+     * Replies the list of {@link Command commands} needed to resolve specified conflicts,
+     * by displaying if necessary a {@link CombinePrimitiveResolverDialog} to the user.
+     * This dialog will allow the user to choose conflict resolution actions.
+     * <p>
+     * Non-expert users are informed first of the meaning of these operations, allowing them to cancel.
+     *
+     * @param tagsOfPrimitives The tag collection of the primitives to be combined.
+     *                         Should generally be equal to {@code TagCollection.unionOfAllPrimitives(primitives)}
+     * @param primitives The primitives to be combined
+     * @param targetPrimitives The primitives the collection of primitives are merged or combined to.
+     * @pararm strategy The strategy to use when merging primitives
+     * @return The list of {@link Command commands} needed to apply resolution actions.
+     * @throws UserCancelException If the user cancelled a dialog.
+     * @since xxx
+     */
+    public static List<Command> launchIfNecessary(
+            final TagCollection tagsOfPrimitives,
+            final Collection<? extends OsmPrimitive> primitives,
+            final Collection<? extends OsmPrimitive> targetPrimitives,
+            Strategy strategy) throws UserCancelException {
         CheckParameterUtil.ensureParameterNotNull(tagsOfPrimitives, "tagsOfPrimitives");
         CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
         CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives");
@@ -518,7 +557,7 @@
 
         tagModel.populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues(), false);
         relModel.populate(parentRelations, primitives, false);
-        if (Config.getPref().getBoolean("combine-conflict-precise", true)) {
+        if (Strategy.ASK == strategy && Config.getPref().getBoolean("combine-conflict-precise", true)) {
             tagModel.prepareDefaultTagDecisions(getResolvableKeys(tagsOfPrimitives.getKeys(), primitives));
         } else {
             tagModel.prepareDefaultTagDecisions(false);
Index: plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/replacegeometry/ReplaceGeometryUtils.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/replacegeometry/ReplaceGeometryUtils.java b/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/replacegeometry/ReplaceGeometryUtils.java
--- a/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/replacegeometry/ReplaceGeometryUtils.java	(revision 36231)
+++ b/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/replacegeometry/ReplaceGeometryUtils.java	(date 1711050996643)
@@ -81,15 +81,28 @@
      * @return (in case of success) a command to update the geometry of fist object and remove the other
      */
     public static ReplaceGeometryCommand buildReplaceCommand(OsmPrimitive subjectObject, OsmPrimitive referenceSubject) {
+        return buildReplaceCommand(subjectObject, referenceSubject, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
+
+    /**
+     * Replace subjectObject geometry with referenceObject geometry and merge tags
+     * and relation memberships.
+     * @param subjectObject object to modify
+     * @param referenceSubject object that gives new geometry and is removed
+     * @param strategy The strategy to use when resolving conflicts
+     * @return (in case of success) a command to update the geometry of fist object and remove the other
+     */
+    public static ReplaceGeometryCommand buildReplaceCommand(OsmPrimitive subjectObject, OsmPrimitive referenceSubject,
+                                                             CombinePrimitiveResolverDialog.Strategy strategy) {
         if (subjectObject instanceof Node && referenceSubject instanceof Node) {
-            return buildReplaceNodeCommand((Node) subjectObject, (Node) referenceSubject);
+            return buildReplaceNodeCommand((Node) subjectObject, (Node) referenceSubject, strategy);
         } else if (subjectObject instanceof Way && referenceSubject instanceof Way) {
-            return buildReplaceWayCommand((Way) subjectObject, (Way) referenceSubject);
+            return buildReplaceWayCommand((Way) subjectObject, (Way) referenceSubject, strategy);
         } else if (subjectObject instanceof Node) {
-            return buildUpgradeNodeCommand((Node) subjectObject, referenceSubject);
+            return buildUpgradeNodeCommand((Node) subjectObject, referenceSubject, strategy);
         } else if (referenceSubject instanceof Node) {
             // TODO: fix this illogical reversal?
-            return buildUpgradeNodeCommand((Node) referenceSubject, subjectObject);
+            return buildUpgradeNodeCommand((Node) referenceSubject, subjectObject, strategy);
         } else {
             throw new IllegalArgumentException(
                     tr("This tool can only replace a node, upgrade a node to a way or a multipolygon, or replace a way with a way."));
@@ -120,13 +133,25 @@
      * @return command to replace node or null if user cancelled
      */
     public static ReplaceGeometryCommand buildReplaceNodeCommand(Node subjectNode, Node referenceNode) {
+        return buildUpgradeNodeCommand(subjectNode, referenceNode, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
+
+    /**
+     * Replace a node with another node (similar to MergeNodesAction)
+     * @param subjectNode node to be replaced
+     * @param referenceNode node with greater spatial quality
+     * @param strategy The strategy to use when resolving conflicts
+     * @return command to replace node or null if user cancelled
+     */
+    public static ReplaceGeometryCommand buildReplaceNodeCommand(Node subjectNode, Node referenceNode,
+                                                                 CombinePrimitiveResolverDialog.Strategy strategy) {
         if (!subjectNode.getParentWays().isEmpty()) {
             throw new ReplaceGeometryException(tr("Node belongs to way(s), cannot replace."));
         }
         // FIXME: handle different layers
         List<Command> commands = new ArrayList<>();
         Command c = MergeNodesAction.mergeNodes(
-            Arrays.asList(subjectNode, referenceNode), referenceNode);
+            Arrays.asList(subjectNode, referenceNode), referenceNode, strategy);
         if (c == null) {
             // User cancelled
             return null;
@@ -146,11 +171,24 @@
      * @return command to replace
      */
     public static ReplaceGeometryCommand buildUpgradeNodeCommand(Node subjectNode, OsmPrimitive referenceObject) {
+        return buildUpgradeNodeCommand(subjectNode, referenceObject, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
+
+    /**
+     * Upgrade a node to a way or multipolygon
+     *
+     * @param subjectNode node to be replaced
+     * @param referenceObject object with greater spatial quality
+     * @param strategy The strategy to use when resolving tag conflicts
+     * @return command to replace
+     */
+    public static ReplaceGeometryCommand buildUpgradeNodeCommand(Node subjectNode, OsmPrimitive referenceObject,
+                                                                 CombinePrimitiveResolverDialog.Strategy strategy) {
         if (!subjectNode.getParentWays().isEmpty()) {
             throw new ReplaceGeometryException(tr("Node belongs to way(s), cannot replace."));
         }
 
-        if (referenceObject instanceof Relation && !((Relation) referenceObject).isMultipolygon()) {
+        if (referenceObject instanceof Relation && !referenceObject.isMultipolygon()) {
             throw new ReplaceGeometryException(tr("Relation is not a multipolygon, cannot be used as a replacement."));
         }
 
@@ -180,12 +218,12 @@
             }
         }
 
-        List<Command> commands = new ArrayList<>();
+        List<Command> commands;
         AbstractMap<String, String> nodeTags = subjectNode.getKeys();
 
         // merge tags
         try {
-            commands.addAll(getTagConflictResolutionCommands(subjectNode, referenceObject));
+            commands = new ArrayList<>(getTagConflictResolutionCommands(subjectNode, referenceObject, strategy));
         } catch (UserCancelException e) {
             // user cancelled tag merge dialog
             return null;
@@ -266,7 +304,18 @@
      * @return Command to replace geometry or null if user cancelled
      */
     public static ReplaceGeometryCommand buildReplaceWayCommand(Way subjectWay, Way referenceWay) {
+        return buildReplaceWayCommand(subjectWay, referenceWay, CombinePrimitiveResolverDialog.Strategy.ASK);
+    }
 
+    /**
+     * Replace geometry of subjectWay by that of referenceWay. Tries to keep the history of nodes.
+     * @param subjectWay way to modify
+     * @param referenceWay way to remove
+     * @param strategy The strategy to use when resolving conflicts
+     * @return Command to replace geometry or null if user cancelled
+     */
+    public static ReplaceGeometryCommand buildReplaceWayCommand(Way subjectWay, Way referenceWay,
+                                                                CombinePrimitiveResolverDialog.Strategy strategy) {
         Area a = MainApplication.getLayerManager().getEditDataSet().getDataSourceArea();
         if (!isInArea(subjectWay, a) || !isInArea(referenceWay, a)) {
             throw new ReplaceGeometryException(tr("The ways must be entirely within the downloaded area."));
@@ -281,7 +330,7 @@
 
         // merge tags
         try {
-            commands.addAll(getTagConflictResolutionCommands(referenceWay, subjectWay));
+            commands.addAll(getTagConflictResolutionCommands(referenceWay, subjectWay, strategy));
         } catch (UserCancelException e) {
             // user cancelled tag merge dialog
             Logging.trace(e);
@@ -472,14 +521,16 @@
      *
      * @param source object tags are merged from
      * @param target object tags are merged to
+     * @param strategy The strategy to use when resolving conflicts
      * @return The list of {@link Command commands} needed to apply resolution actions.
      * @throws UserCancelException If the user cancelled a dialog.
      */
-    static List<Command> getTagConflictResolutionCommands(OsmPrimitive source, OsmPrimitive target) throws UserCancelException {
+    static List<Command> getTagConflictResolutionCommands(OsmPrimitive source, OsmPrimitive target,
+                                                          CombinePrimitiveResolverDialog.Strategy strategy) throws UserCancelException {
         Collection<OsmPrimitive> primitives = Arrays.asList(source, target);
         // launch a conflict resolution dialog, if necessary
         return CombinePrimitiveResolverDialog.launchIfNecessary(
-                TagCollection.unionOfAllPrimitives(primitives), primitives, Collections.singleton(target));
+                TagCollection.unionOfAllPrimitives(primitives), primitives, Collections.singleton(target), strategy);
     }
 
     /**
