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