001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 import static org.openstreetmap.josm.tools.I18n.trn; 007 008 import java.awt.event.ActionEvent; 009 import java.awt.event.KeyEvent; 010 import java.util.ArrayList; 011 import java.util.Collection; 012 import java.util.Collections; 013 import java.util.HashSet; 014 import java.util.LinkedList; 015 import java.util.List; 016 import java.util.Set; 017 018 import javax.swing.JOptionPane; 019 import javax.swing.JPanel; 020 021 import org.openstreetmap.josm.Main; 022 import org.openstreetmap.josm.command.AddCommand; 023 import org.openstreetmap.josm.command.ChangeCommand; 024 import org.openstreetmap.josm.command.Command; 025 import org.openstreetmap.josm.command.SequenceCommand; 026 import org.openstreetmap.josm.data.osm.Node; 027 import org.openstreetmap.josm.data.osm.OsmPrimitive; 028 import org.openstreetmap.josm.data.osm.Relation; 029 import org.openstreetmap.josm.data.osm.RelationMember; 030 import org.openstreetmap.josm.data.osm.Way; 031 import org.openstreetmap.josm.gui.MapView; 032 import org.openstreetmap.josm.tools.Shortcut; 033 034 /** 035 * Duplicate nodes that are used by multiple ways. 036 * 037 * Resulting nodes are identical, up to their position. 038 * 039 * This is the opposite of the MergeNodesAction. 040 * 041 * If a single node is selected, it will copy that node and remove all tags from the old one 042 */ 043 044 public class UnGlueAction extends JosmAction { 045 046 private Node selectedNode; 047 private Way selectedWay; 048 private Set<Node> selectedNodes; 049 050 /** 051 * Create a new UnGlueAction. 052 */ 053 public UnGlueAction() { 054 super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."), 055 Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.DIRECT), true); 056 putValue("help", ht("/Action/UnGlue")); 057 } 058 059 /** 060 * Called when the action is executed. 061 * 062 * This method does some checking on the selection and calls the matching unGlueWay method. 063 */ 064 @Override 065 public void actionPerformed(ActionEvent e) { 066 067 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); 068 069 String errMsg = null; 070 if (checkSelection(selection)) { 071 if (!checkAndConfirmOutlyingUnglue()) { 072 return; 073 } 074 int count = 0; 075 for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { 076 if (!w.isUsable() || w.getNodesCount() < 1) { 077 continue; 078 } 079 count++; 080 } 081 if (count < 2) { 082 // If there aren't enough ways, maybe the user wanted to unglue the nodes 083 // (= copy tags to a new node) 084 if (checkForUnglueNode(selection)) { 085 unglueNode(e); 086 } else { 087 errMsg = tr("This node is not glued to anything else."); 088 } 089 } else { 090 // and then do the work. 091 unglueWays(); 092 } 093 } else if (checkSelection2(selection)) { 094 if (!checkAndConfirmOutlyingUnglue()) { 095 return; 096 } 097 Set<Node> tmpNodes = new HashSet<Node>(); 098 for (Node n : selectedNodes) { 099 int count = 0; 100 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 101 if (!w.isUsable()) { 102 continue; 103 } 104 count++; 105 } 106 if (count >= 2) { 107 tmpNodes.add(n); 108 } 109 } 110 if (tmpNodes.size() < 1) { 111 if (selection.size() > 1) { 112 errMsg = tr("None of these nodes are glued to anything else."); 113 } else { 114 errMsg = tr("None of this way''s nodes are glued to anything else."); 115 } 116 } else { 117 // and then do the work. 118 selectedNodes = tmpNodes; 119 unglueWays2(); 120 } 121 } else { 122 errMsg = 123 tr("The current selection cannot be used for unglueing.")+"\n"+ 124 "\n"+ 125 tr("Select either:")+"\n"+ 126 tr("* One tagged node, or")+"\n"+ 127 tr("* One node that is used by more than one way, or")+"\n"+ 128 tr("* One node that is used by more than one way and one of those ways, or")+"\n"+ 129 tr("* One way that has one or more nodes that are used by more than one way, or")+"\n"+ 130 tr("* One way and one or more of its nodes that are used by more than one way.")+"\n"+ 131 "\n"+ 132 tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+ 133 "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+ 134 "own copy and all nodes will be selected."); 135 } 136 137 if(errMsg != null) { 138 JOptionPane.showMessageDialog( 139 Main.parent, 140 errMsg, 141 tr("Error"), 142 JOptionPane.ERROR_MESSAGE); 143 } 144 145 selectedNode = null; 146 selectedWay = null; 147 selectedNodes = null; 148 } 149 150 /** 151 * Assumes there is one tagged Node stored in selectedNode that it will try to unglue 152 * (= copy node and remove all tags from the old one. Relations will not be removed) 153 */ 154 private void unglueNode(ActionEvent e) { 155 LinkedList<Command> cmds = new LinkedList<Command>(); 156 157 Node c = new Node(selectedNode); 158 c.removeAll(); 159 getCurrentDataSet().clearSelection(c); 160 cmds.add(new ChangeCommand(selectedNode, c)); 161 162 Node n = new Node(selectedNode, true); 163 164 // If this wasn't called from menu, place it where the cursor is/was 165 if(e.getSource() instanceof JPanel) { 166 MapView mv = Main.map.mapView; 167 n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY())); 168 } 169 170 cmds.add(new AddCommand(n)); 171 172 fixRelations(selectedNode, cmds, Collections.singletonList(n)); 173 174 Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds)); 175 getCurrentDataSet().setSelected(n); 176 Main.map.mapView.repaint(); 177 } 178 179 /** 180 * Checks if selection is suitable for ungluing. This is the case when there's a single, 181 * tagged node selected that's part of at least one way (ungluing an unconnected node does 182 * not make sense. Due to the call order in actionPerformed, this is only called when the 183 * node is only part of one or less ways. 184 * 185 * @param The selection to check against 186 * @return Selection is suitable 187 */ 188 private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) { 189 if (selection.size() != 1) 190 return false; 191 OsmPrimitive n = (OsmPrimitive) selection.toArray()[0]; 192 if (!(n instanceof Node)) 193 return false; 194 if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty()) 195 return false; 196 197 selectedNode = (Node) n; 198 return selectedNode.isTagged(); 199 } 200 201 /** 202 * Checks if the selection consists of something we can work with. 203 * Checks only if the number and type of items selected looks good. 204 * 205 * If this method returns "true", selectedNode and selectedWay will 206 * be set. 207 * 208 * Returns true if either one node is selected or one node and one 209 * way are selected and the node is part of the way. 210 * 211 * The way will be put into the object variable "selectedWay", the 212 * node into "selectedNode". 213 */ 214 private boolean checkSelection(Collection<? extends OsmPrimitive> selection) { 215 216 int size = selection.size(); 217 if (size < 1 || size > 2) 218 return false; 219 220 selectedNode = null; 221 selectedWay = null; 222 223 for (OsmPrimitive p : selection) { 224 if (p instanceof Node) { 225 selectedNode = (Node) p; 226 if (size == 1 || selectedWay != null) 227 return size == 1 || selectedWay.containsNode(selectedNode); 228 } else if (p instanceof Way) { 229 selectedWay = (Way) p; 230 if (size == 2 && selectedNode != null) 231 return selectedWay.containsNode(selectedNode); 232 } 233 } 234 235 return false; 236 } 237 238 /** 239 * Checks if the selection consists of something we can work with. 240 * Checks only if the number and type of items selected looks good. 241 * 242 * Returns true if one way and any number of nodes that are part of 243 * that way are selected. Note: "any" can be none, then all nodes of 244 * the way are used. 245 * 246 * The way will be put into the object variable "selectedWay", the 247 * nodes into "selectedNodes". 248 */ 249 private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) { 250 if (selection.size() < 1) 251 return false; 252 253 selectedWay = null; 254 for (OsmPrimitive p : selection) { 255 if (p instanceof Way) { 256 if (selectedWay != null) 257 return false; 258 selectedWay = (Way) p; 259 } 260 } 261 if (selectedWay == null) 262 return false; 263 264 selectedNodes = new HashSet<Node>(); 265 for (OsmPrimitive p : selection) { 266 if (p instanceof Node) { 267 Node n = (Node) p; 268 if (!selectedWay.containsNode(n)) 269 return false; 270 selectedNodes.add(n); 271 } 272 } 273 274 if (selectedNodes.size() < 1) { 275 selectedNodes.addAll(selectedWay.getNodes()); 276 } 277 278 return true; 279 } 280 281 /** 282 * dupe the given node of the given way 283 * 284 * assume that OrginalNode is in the way 285 * 286 * -> the new node will be put into the parameter newNodes. 287 * -> the add-node command will be put into the parameter cmds. 288 * -> the changed way will be returned and must be put into cmds by the caller! 289 */ 290 private Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { 291 // clone the node for the way 292 Node newNode = new Node(originalNode, true /* clear OSM ID */); 293 newNodes.add(newNode); 294 cmds.add(new AddCommand(newNode)); 295 296 ArrayList<Node> nn = new ArrayList<Node>(); 297 for (Node pushNode : w.getNodes()) { 298 if (originalNode == pushNode) { 299 pushNode = newNode; 300 } 301 nn.add(pushNode); 302 } 303 Way newWay = new Way(w); 304 newWay.setNodes(nn); 305 306 return newWay; 307 } 308 309 /** 310 * put all newNodes into the same relation(s) that originalNode is in 311 */ 312 private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) { 313 // modify all relations containing the node 314 Relation newRel = null; 315 HashSet<String> rolesToReAdd = null; 316 for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) { 317 if (r.isDeleted()) { 318 continue; 319 } 320 newRel = null; 321 rolesToReAdd = null; 322 for (RelationMember rm : r.getMembers()) { 323 if (rm.isNode()) { 324 if (rm.getMember() == originalNode) { 325 if (newRel == null) { 326 newRel = new Relation(r); 327 rolesToReAdd = new HashSet<String>(); 328 } 329 rolesToReAdd.add(rm.getRole()); 330 } 331 } 332 } 333 if (newRel != null) { 334 for (Node n : newNodes) { 335 for (String role : rolesToReAdd) { 336 newRel.addMember(new RelationMember(role, n)); 337 } 338 } 339 cmds.add(new ChangeCommand(r, newRel)); 340 } 341 } 342 } 343 344 /** 345 * dupe a single node into as many nodes as there are ways using it, OR 346 * 347 * dupe a single node once, and put the copy on the selected way 348 */ 349 private void unglueWays() { 350 LinkedList<Command> cmds = new LinkedList<Command>(); 351 LinkedList<Node> newNodes = new LinkedList<Node>(); 352 353 if (selectedWay == null) { 354 Way wayWithSelectedNode = null; 355 LinkedList<Way> parentWays = new LinkedList<Way>(); 356 for (OsmPrimitive osm : selectedNode.getReferrers()) { 357 if (osm.isUsable() && osm instanceof Way) { 358 Way w = (Way) osm; 359 if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) { 360 wayWithSelectedNode = w; 361 } else { 362 parentWays.add(w); 363 } 364 } 365 } 366 if (wayWithSelectedNode == null) { 367 wayWithSelectedNode = parentWays.removeFirst(); 368 } 369 for (Way w : parentWays) { 370 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); 371 } 372 } else { 373 cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes))); 374 } 375 376 fixRelations(selectedNode, cmds, newNodes); 377 378 Main.main.undoRedo.add(new SequenceCommand(tr("Dupe into {0} nodes", newNodes.size()+1), cmds)); 379 // select one of the new nodes 380 getCurrentDataSet().setSelected(newNodes.getFirst()); 381 } 382 383 /** 384 * dupe all nodes that are selected, and put the copies on the selected way 385 * 386 */ 387 private void unglueWays2() { 388 LinkedList<Command> cmds = new LinkedList<Command>(); 389 List<Node> allNewNodes = new LinkedList<Node>(); 390 Way tmpWay = selectedWay; 391 392 for (Node n : selectedNodes) { 393 List<Node> newNodes = new LinkedList<Node>(); 394 tmpWay = modifyWay(n, tmpWay, cmds, newNodes); 395 fixRelations(n, cmds, newNodes); 396 allNewNodes.addAll(newNodes); 397 } 398 cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen 399 400 Main.main.undoRedo.add(new SequenceCommand( 401 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds)); 402 getCurrentDataSet().setSelected(allNewNodes); 403 } 404 405 @Override 406 protected void updateEnabledState() { 407 if (getCurrentDataSet() == null) { 408 setEnabled(false); 409 } else { 410 updateEnabledState(getCurrentDataSet().getSelected()); 411 } 412 } 413 414 @Override 415 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 416 setEnabled(selection != null && !selection.isEmpty()); 417 } 418 419 protected boolean checkAndConfirmOutlyingUnglue() { 420 List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>(2 + (selectedNodes == null ? 0 : selectedNodes.size())); 421 if (selectedNodes != null) 422 primitives.addAll(selectedNodes); 423 if (selectedNode != null) 424 primitives.add(selectedNode); 425 return Command.checkAndConfirmOutlyingOperation("unglue", 426 tr("Unglue confirmation"), 427 tr("You are about to unglue nodes outside of the area you have downloaded." 428 + "<br>" 429 + "This can cause problems because other objects (that you do not see) might use them." 430 + "<br>" 431 + "Do you really want to unglue?"), 432 tr("You are about to unglue incomplete objects." 433 + "<br>" 434 + "This will cause problems because you don''t see the real object." 435 + "<br>" + "Do you really want to unglue?"), 436 getEditLayer().data.getDataSourceArea(), primitives, null); 437 } 438 }