//License: GPL. Copyright 2007 by Immanuel Scholz and others. See LICENSE file for details. package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.ChangeNodesCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.RelationToChildReference; import org.openstreetmap.josm.data.osm.TagCollection; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.DefaultNameFormatter; import org.openstreetmap.josm.gui.HelpAwareOptionPane; import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; /** * Merges a collection of nodes into one node. * * The "surviving" node will be the one with the lowest positive id. * (I.e. it was uploaded to the server and is the oldest one.) * * However we use the location of the node that was selected *last*. * The "surviving" node will be moved to that location if it is * different from the last selected node. */ public class MergeNodesAction extends JosmAction { public MergeNodesAction() { super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."), Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true); putValue("help", ht("/Action/MergeNodesAction")); } public void actionPerformed(ActionEvent event) { if (!isEnabled()) return; Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); LinkedHashSet<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class); if (selectedNodes.size() < 2) { JOptionPane.showMessageDialog( Main.parent, tr("Please select at least two nodes to merge."), tr("Warning"), JOptionPane.WARNING_MESSAGE ); return; } Node targetNode = selectTargetNode(selectedNodes); Node targetLocationNode = selectTargetLocationNode(selectedNodes); Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode, targetLocationNode); if (cmd != null) { Main.main.undoRedo.add(cmd); Main.main.getEditLayer().data.setSelected(targetNode); } } /** * Select the location of the target node after merge. * * @param candidates the collection of candidate nodes * @return the coordinates of this node are later used for the target node */ public static Node selectTargetLocationNode(LinkedHashSet<Node> candidates) { Node targetNode = null; for (Node n : candidates) { // pick last one targetNode = n; } return targetNode; } /** * Find which node to merge into (i.e. which one will be left) * * @param candidates the collection of candidate nodes * @return the selected target node */ public static Node selectTargetNode(LinkedHashSet<Node> candidates) { Node targetNode = null; Node lastNode = null; for (Node n : candidates) { if (!n.isNew()) { if (targetNode == null) { targetNode = n; } else if (n.getId() < targetNode.getId()) { targetNode = n; } } lastNode = n; } if (targetNode == null) { targetNode = lastNode; } return targetNode; } /** * Fixes the parent ways referring to one of the nodes. * * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted * which is referred to by a relation. * * @param nodesToDelete the collection of nodes to be deleted * @param targetNode the target node the other nodes are merged to * @return a list of commands; null, if the ways could not be fixed */ protected static List<Command> fixParentWays(Collection<Node> nodesToDelete, Node targetNode) { List<Command> cmds = new ArrayList<Command>(); Set<Way> waysToDelete = new HashSet<Way>(); for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) { ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount()); for (Node n: w.getNodes()) { if (! nodesToDelete.contains(n) && n != targetNode) { newNodes.add(n); } else if (newNodes.isEmpty()) { newNodes.add(targetNode); } else if (newNodes.get(newNodes.size()-1) != targetNode) { // make sure we collapse a sequence of deleted nodes // to exactly one occurrence of the merged target node // newNodes.add(targetNode); } else { // drop the node } } if (newNodes.size() < 2) { if (w.getReferrers().isEmpty()) { waysToDelete.add(w); } else { ButtonSpec[] options = new ButtonSpec[] { new ButtonSpec( tr("Abort Merging"), ImageProvider.get("cancel"), tr("Click to abort merging nodes"), null /* no special help topic */ ) }; HelpAwareOptionPane.showOptionDialog( Main.parent, tr( "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.", w.getDisplayName(DefaultNameFormatter.getInstance()) ), tr("Warning"), JOptionPane.WARNING_MESSAGE, null, /* no icon */ options, options[0], ht("/Action/MergeNodes#WaysToDeleteStillInUse") ); return null; } } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) { waysToDelete.add(w); } else { cmds.add(new ChangeNodesCommand(w, newNodes)); } } if (!waysToDelete.isEmpty()) { cmds.add(new DeleteCommand(waysToDelete)); } return cmds; } public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) { return mergeNodes(layer, nodes, targetNode, targetNode); } /** * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset * managed by <code>layer</code> as reference. * * @param layer layer the reference data layer. Must not be null. * @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. * @throw IllegalArgumentException thrown if layer is null */ public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode, Node targetLocationNode) { CheckParameterUtil.ensureParameterNotNull(layer, "layer"); CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode"); if (nodes == null) return null; Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes); // build the tag collection // TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes); combineTigerTags(nodeTags); normalizeTagCollectionBeforeEditing(nodeTags, nodes); TagCollection nodeTagsToEdit = new TagCollection(nodeTags); completeTagCollectionForEditing(nodeTagsToEdit); // launch a conflict resolution dialog, if necessary // CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); dialog.getTagConflictResolverModel().populate(nodeTagsToEdit, nodeTags.getKeysWithMultipleValues()); dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences); dialog.setTargetPrimitive(targetNode); dialog.prepareDefaultDecisions(); // conflict resolution is necessary if there are conflicts in the merged tags // or if at least one of the merged nodes is referred to by a relation // if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) { dialog.setVisible(true); if (dialog.isCancelled()) return null; } LinkedList<Command> cmds = new LinkedList<Command>(); // the nodes we will have to delete // Collection<Node> nodesToDelete = new HashSet<Node>(nodes); nodesToDelete.remove(targetNode); // fix the ways referring to at least one of the merged nodes // Collection<Way> waysToDelete= new HashSet<Way>(); List<Command> wayFixCommands = fixParentWays( nodesToDelete, targetNode ); if (wayFixCommands == null) return null; cmds.addAll(wayFixCommands); // build the commands // if (targetNode != targetLocationNode) { Node newTargetNode = new Node(targetNode); newTargetNode.setCoor(targetLocationNode.getCoor()); cmds.add(new ChangeCommand(targetNode, newTargetNode)); } if (!nodesToDelete.isEmpty()) { cmds.add(new DeleteCommand(nodesToDelete)); } if (!waysToDelete.isEmpty()) { cmds.add(new DeleteCommand(waysToDelete)); } cmds.addAll(dialog.buildResolutionCommands()); Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds); return cmd; } @Override protected void updateEnabledState() { if (getCurrentDataSet() == null) { setEnabled(false); } else { updateEnabledState(getCurrentDataSet().getSelected()); } } @Override protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { if (selection == null || selection.isEmpty()) { setEnabled(false); return; } boolean ok = true; if (selection.size() < 2) { setEnabled(false); return; } for (OsmPrimitive osm : selection) { if (!(osm instanceof Node)) { ok = false; break; } } setEnabled(ok); } }