// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; 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.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.tools.Shortcut; /** * Duplicate nodes that are used by multiple ways. * * Resulting nodes are identical, up to their position. * * This is the opposite of the MergeNodesAction. * * If a single node is selected, it will copy that node and remove all tags from the old one */ public class UnGlueAction extends JosmAction { private Node selectedNode; private Way selectedWay; private ArrayList<Node> selectedNodes; /** * Create a new UnGlueAction. */ public UnGlueAction() { super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."), Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.GROUP_EDIT), true); putValue("help", ht("/Action/UnGlue")); } /** * Called when the action is executed. * * This method does some checking on the selection and calls the matching unGlueWay method. */ public void actionPerformed(ActionEvent e) { Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); String errMsg = null; if (checkSelection(selection)) { int count = 0; for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { if (!w.isUsable() || w.getNodesCount() < 1) { continue; } count++; } if (count < 2) { // If there aren't enough ways, maybe the user wanted to unglue the nodes // (= copy tags to a new node) if (checkForUnglueNode(selection)) { unglueNode(e); } else { errMsg = tr("This node is not glued to anything else."); } } else { // and then do the work. unglueWays(); } } else if (checkSelection2(selection)) { ArrayList<Node> tmpNodes = new ArrayList<Node>(); for (Node n : selectedNodes) { int count = 0; for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { if (!w.isUsable()) { continue; } count++; } if (count >= 2) { tmpNodes.add(n); } } if (tmpNodes.size() < 1) { if (selection.size() > 1) { errMsg = tr("None of these nodes are glued to anything else."); } else { errMsg = tr("None of this way''s nodes are glued to anything else."); } } else { // and then do the work. selectedNodes = tmpNodes; unglueWays2(); } } else { errMsg = tr("The current selection cannot be used for unglueing.")+"\n"+ "\n"+ tr("Select either:")+"\n"+ tr("* One tagged node, or")+"\n"+ tr("* One node that is used by more than one way, or")+"\n"+ tr("* One node that is used by more than one way and one of those ways, or")+"\n"+ tr("* One way that has one or more nodes that are used by more than one way, or")+"\n"+ tr("* One way and one or more of its nodes that are used by more than one way.")+"\n"+ "\n"+ tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+ "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+ "own copy and all nodes will be selected."); } if(errMsg != null) { JOptionPane.showMessageDialog( Main.parent, errMsg, tr("Error"), JOptionPane.ERROR_MESSAGE); } selectedNode = null; selectedWay = null; selectedNodes = null; } /** * Assumes there is one tagged Node stored in selectedNode that it will try to unglue * (= copy node and remove all tags from the old one. Relations will not be removed) */ private void unglueNode(ActionEvent e) { LinkedList<Command> cmds = new LinkedList<Command>(); Node c = new Node(selectedNode); c.removeAll(); getCurrentDataSet().clearSelection(c); cmds.add(new ChangeCommand(selectedNode, c)); Node n = new Node(selectedNode, true); // If this wasn't called from menu, place it where the cursor is/was if(e.getSource() instanceof JPanel) { MapView mv = Main.map.mapView; n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY())); } cmds.add(new AddCommand(n)); fixRelations(selectedNode, cmds, Collections.singletonList(n)); Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds)); getCurrentDataSet().setSelected(n); Main.map.mapView.repaint(); } /** * Checks if selection is suitable for ungluing. This is the case when there's a single, * tagged node selected that's part of at least one way (ungluing an unconnected node does * not make sense. Due to the call order in actionPerformed, this is only called when the * node is only part of one or less ways. * * @param The selection to check against * @return Selection is suitable */ private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) { if (selection.size() != 1) return false; OsmPrimitive n = (OsmPrimitive) selection.toArray()[0]; if (!(n instanceof Node)) return false; if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty()) return false; selectedNode = (Node) n; return selectedNode.isTagged(); } /** * Checks if the selection consists of something we can work with. * Checks only if the number and type of items selected looks good; * does not check whether the selected items are really a valid * input for splitting (this would be too expensive to be carried * out from the selectionChanged listener). * * If this method returns "true", selectedNode and selectedWay will * be set. * * Returns true if either one node is selected or one node and one * way are selected and the node is part of the way. * * The way will be put into the object variable "selectedWay", the * node into "selectedNode". */ private boolean checkSelection(Collection<? extends OsmPrimitive> selection) { int size = selection.size(); if (size < 1 || size > 2) return false; selectedNode = null; selectedWay = null; for (OsmPrimitive p : selection) { if (p instanceof Node) { selectedNode = (Node) p; if (size == 1 || selectedWay != null) return size == 1 || selectedWay.containsNode(selectedNode); } else if (p instanceof Way) { selectedWay = (Way) p; if (size == 2 && selectedNode != null) return selectedWay.containsNode(selectedNode); } } return false; } /** * Checks if the selection consists of something we can work with. * Checks only if the number and type of items selected looks good; * does not check whether the selected items are really a valid * input for splitting (this would be too expensive to be carried * out from the selectionChanged listener). * * Returns true if one way and any number of nodes that are part of * that way are selected. Note: "any" can be none, then all nodes of * the way are used. * * The way will be put into the object variable "selectedWay", the * nodes into "selectedNodes". */ private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) { if (selection.size() < 1) return false; selectedWay = null; for (OsmPrimitive p : selection) { if (p instanceof Way) { if (selectedWay != null) return false; selectedWay = (Way) p; } } if (selectedWay == null) return false; selectedNodes = new ArrayList<Node>(); for (OsmPrimitive p : selection) { if (p instanceof Node) { Node n = (Node) p; if (!selectedWay.containsNode(n)) return false; selectedNodes.add(n); } } if (selectedNodes.size() < 1) { selectedNodes.addAll(selectedWay.getNodes()); } return true; } /** * dupe the given node of the given way * * -> the new node will be put into the parameter newNodes. * -> the add-node command will be put into the parameter cmds. * -> the changed way will be returned and must be put into cmds by the caller! */ private Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { ArrayList<Node> nn = new ArrayList<Node>(); for (Node pushNode : w.getNodes()) { if (originalNode == pushNode) { // clone the node for all other ways pushNode = new Node(pushNode, true /* clear OSM ID */); newNodes.add(pushNode); cmds.add(new AddCommand(pushNode)); } nn.add(pushNode); } Way newWay = new Way(w); newWay.setNodes(nn); return newWay; } /** * put all newNodes into the same relation(s) that originalNode is in */ private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) { // modify all relations containing the node Relation newRel = null; HashSet<String> rolesToReAdd = null; for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) { if (r.isDeleted()) { continue; } newRel = null; rolesToReAdd = null; for (RelationMember rm : r.getMembers()) { if (rm.isNode()) { if (rm.getMember() == originalNode) { if (newRel == null) { newRel = new Relation(r); newRel.setMembers(null); rolesToReAdd = new HashSet<String>(); } rolesToReAdd.add(rm.getRole()); } } } if (newRel != null) { for (RelationMember rm : r.getMembers()) { newRel.addMember(rm); } for (Node n : newNodes) { for (String role : rolesToReAdd) { newRel.addMember(new RelationMember(role, n)); } } cmds.add(new ChangeCommand(r, newRel)); } } } /** * dupe a single node into as many nodes as there are ways using it, OR * * dupe a single node once, and put the copy on the selected way */ private void unglueWays() { LinkedList<Command> cmds = new LinkedList<Command>(); List<Node> newNodes = new LinkedList<Node>(); if (selectedWay == null) { boolean firstway = true; // modify all ways containing the nodes for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { if (w.isDeleted() || w.isIncomplete()) { continue; } if (!firstway) { cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); } firstway = false; } } else { cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes))); } fixRelations(selectedNode, cmds, newNodes); Main.main.undoRedo.add(new SequenceCommand(tr("Dupe into {0} nodes", newNodes.size()+1), cmds)); if (selectedWay == null) { // if a node has been selected, new selection is ALL nodes newNodes.add(selectedNode); } // if a node and a way has been selected, new selection is only the new node that was added to the selected way getCurrentDataSet().setSelected(newNodes); } /** * dupe all nodes that are selected, and put the copies on the selected way * */ private void unglueWays2() { LinkedList<Command> cmds = new LinkedList<Command>(); List<Node> allNewNodes = new LinkedList<Node>(); Way tmpWay = selectedWay; for (Node n : selectedNodes) { List<Node> newNodes = new LinkedList<Node>(); tmpWay = modifyWay(n, tmpWay, cmds, newNodes); fixRelations(n, cmds, newNodes); allNewNodes.addAll(newNodes); } cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen Main.main.undoRedo.add(new SequenceCommand( trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds)); getCurrentDataSet().setSelected(allNewNodes); } @Override protected void updateEnabledState() { if (getCurrentDataSet() == null) { setEnabled(false); } else { updateEnabledState(getCurrentDataSet().getSelected()); } } @Override protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { setEnabled(selection != null && !selection.isEmpty()); } }