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 transient Node selectedNode; 050 private transient Way selectedWay; 051 private transient 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.isEmpty()) { 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 List<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 * @return true if either one node is selected or one node and one way are selected and the node is part of the way 228 */ 229 private boolean checkSelection(Collection<? extends OsmPrimitive> selection) { 230 231 int size = selection.size(); 232 if (size < 1 || size > 2) 233 return false; 234 235 selectedNode = null; 236 selectedWay = null; 237 238 for (OsmPrimitive p : selection) { 239 if (p instanceof Node) { 240 selectedNode = (Node) p; 241 if (size == 1 || selectedWay != null) 242 return size == 1 || selectedWay.containsNode(selectedNode); 243 } else if (p instanceof Way) { 244 selectedWay = (Way) p; 245 if (size == 2 && selectedNode != null) 246 return selectedWay.containsNode(selectedNode); 247 } 248 } 249 250 return false; 251 } 252 253 /** 254 * Checks if the selection consists of something we can work with. 255 * Checks only if the number and type of items selected looks good. 256 * 257 * Returns true if one way and any number of nodes that are part of 258 * that way are selected. Note: "any" can be none, then all nodes of 259 * the way are used. 260 * 261 * The way will be put into the object variable "selectedWay", the 262 * nodes into "selectedNodes". 263 * @return true if one way and any number of nodes that are part of that way are selected 264 */ 265 private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) { 266 if (selection.isEmpty()) 267 return false; 268 269 selectedWay = null; 270 for (OsmPrimitive p : selection) { 271 if (p instanceof Way) { 272 if (selectedWay != null) 273 return false; 274 selectedWay = (Way) p; 275 } 276 } 277 if (selectedWay == null) 278 return false; 279 280 selectedNodes = new HashSet<>(); 281 for (OsmPrimitive p : selection) { 282 if (p instanceof Node) { 283 Node n = (Node) p; 284 if (!selectedWay.containsNode(n)) 285 return false; 286 selectedNodes.add(n); 287 } 288 } 289 290 if (selectedNodes.isEmpty()) { 291 selectedNodes.addAll(selectedWay.getNodes()); 292 } 293 294 return true; 295 } 296 297 /** 298 * dupe the given node of the given way 299 * 300 * assume that OrginalNode is in the way 301 * <ul> 302 * <li>the new node will be put into the parameter newNodes.</li> 303 * <li>the add-node command will be put into the parameter cmds.</li> 304 * <li>the changed way will be returned and must be put into cmds by the caller!</li> 305 * </ul> 306 * @return new way 307 */ 308 private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { 309 // clone the node for the way 310 Node newNode = new Node(originalNode, true /* clear OSM ID */); 311 newNodes.add(newNode); 312 cmds.add(new AddCommand(newNode)); 313 314 List<Node> nn = new ArrayList<>(); 315 for (Node pushNode : w.getNodes()) { 316 if (originalNode == pushNode) { 317 pushNode = newNode; 318 } 319 nn.add(pushNode); 320 } 321 Way newWay = new Way(w); 322 newWay.setNodes(nn); 323 324 return newWay; 325 } 326 327 /** 328 * put all newNodes into the same relation(s) that originalNode is in 329 */ 330 private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) { 331 // modify all relations containing the node 332 for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) { 333 if (r.isDeleted()) { 334 continue; 335 } 336 Relation newRel = null; 337 Map<String, Integer> rolesToReAdd = null; // <role name, index> 338 int i = 0; 339 for (RelationMember rm : r.getMembers()) { 340 if (rm.isNode() && rm.getMember() == originalNode) { 341 if (newRel == null) { 342 newRel = new Relation(r); 343 rolesToReAdd = new HashMap<>(); 344 } 345 rolesToReAdd.put(rm.getRole(), i); 346 } 347 i++; 348 } 349 if (newRel != null) { 350 for (Node n : newNodes) { 351 for (Map.Entry<String, Integer> role : rolesToReAdd.entrySet()) { 352 newRel.addMember(role.getValue() + 1, new RelationMember(role.getKey(), n)); 353 } 354 } 355 cmds.add(new ChangeCommand(r, newRel)); 356 } 357 } 358 } 359 360 /** 361 * dupe a single node into as many nodes as there are ways using it, OR 362 * 363 * dupe a single node once, and put the copy on the selected way 364 */ 365 private void unglueWays() { 366 List<Command> cmds = new LinkedList<>(); 367 List<Node> newNodes = new LinkedList<>(); 368 369 if (selectedWay == null) { 370 Way wayWithSelectedNode = null; 371 LinkedList<Way> parentWays = new LinkedList<>(); 372 for (OsmPrimitive osm : selectedNode.getReferrers()) { 373 if (osm.isUsable() && osm instanceof Way) { 374 Way w = (Way) osm; 375 if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) { 376 wayWithSelectedNode = w; 377 } else { 378 parentWays.add(w); 379 } 380 } 381 } 382 if (wayWithSelectedNode == null) { 383 parentWays.removeFirst(); 384 } 385 for (Way w : parentWays) { 386 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); 387 } 388 } else { 389 cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes))); 390 } 391 392 fixRelations(selectedNode, cmds, newNodes); 393 execCommands(cmds, newNodes); 394 } 395 396 /** 397 * Add commands to undo-redo system. 398 * @param cmds Commands to execute 399 * @param newNodes New created nodes by this set of command 400 */ 401 private static void execCommands(List<Command> cmds, List<Node> newNodes) { 402 Main.main.undoRedo.add(new SequenceCommand(/* for correct i18n of plural forms - see #9110 */ 403 trn("Dupe into {0} node", "Dupe into {0} nodes", newNodes.size() + 1, newNodes.size() + 1), cmds)); 404 // select one of the new nodes 405 getCurrentDataSet().setSelected(newNodes.get(0)); 406 } 407 408 /** 409 * Duplicates a node used several times by the same way. See #9896. 410 * @return true if action is OK false if there is nothing to do 411 */ 412 private boolean unglueSelfCrossingWay() { 413 // According to previous check, only one valid way through that node 414 List<Command> cmds = new LinkedList<>(); 415 Way way = null; 416 for (Way w: OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { 417 if (w.isUsable() && w.getNodesCount() >= 1) { 418 way = w; 419 } 420 } 421 List<Node> oldNodes = way.getNodes(); 422 List<Node> newNodes = new ArrayList<>(oldNodes.size()); 423 List<Node> addNodes = new ArrayList<>(); 424 boolean seen = false; 425 for (Node n: oldNodes) { 426 if (n == selectedNode) { 427 if (seen) { 428 Node newNode = new Node(n, true /* clear OSM ID */); 429 newNodes.add(newNode); 430 cmds.add(new AddCommand(newNode)); 431 newNodes.add(newNode); 432 addNodes.add(newNode); 433 } else { 434 newNodes.add(n); 435 seen = true; 436 } 437 } else { 438 newNodes.add(n); 439 } 440 } 441 if (addNodes.isEmpty()) { 442 // selectedNode doesn't need unglue 443 return false; 444 } 445 cmds.add(new ChangeNodesCommand(way, newNodes)); 446 // Update relation 447 fixRelations(selectedNode, cmds, addNodes); 448 execCommands(cmds, addNodes); 449 return true; 450 } 451 452 /** 453 * dupe all nodes that are selected, and put the copies on the selected way 454 * 455 */ 456 private void unglueWays2() { 457 List<Command> cmds = new LinkedList<>(); 458 List<Node> allNewNodes = new LinkedList<>(); 459 Way tmpWay = selectedWay; 460 461 for (Node n : selectedNodes) { 462 List<Node> newNodes = new LinkedList<>(); 463 tmpWay = modifyWay(n, tmpWay, cmds, newNodes); 464 fixRelations(n, cmds, newNodes); 465 allNewNodes.addAll(newNodes); 466 } 467 cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen 468 469 Main.main.undoRedo.add(new SequenceCommand( 470 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", 471 selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds)); 472 getCurrentDataSet().setSelected(allNewNodes); 473 } 474 475 @Override 476 protected void updateEnabledState() { 477 if (getCurrentDataSet() == null) { 478 setEnabled(false); 479 } else { 480 updateEnabledState(getCurrentDataSet().getSelected()); 481 } 482 } 483 484 @Override 485 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 486 setEnabled(selection != null && !selection.isEmpty()); 487 } 488 489 protected boolean checkAndConfirmOutlyingUnglue() { 490 List<OsmPrimitive> primitives = new ArrayList<>(2 + (selectedNodes == null ? 0 : selectedNodes.size())); 491 if (selectedNodes != null) 492 primitives.addAll(selectedNodes); 493 if (selectedNode != null) 494 primitives.add(selectedNode); 495 return Command.checkAndConfirmOutlyingOperation("unglue", 496 tr("Unglue confirmation"), 497 tr("You are about to unglue nodes outside of the area you have downloaded." 498 + "<br>" 499 + "This can cause problems because other objects (that you do not see) might use them." 500 + "<br>" 501 + "Do you really want to unglue?"), 502 tr("You are about to unglue incomplete objects." 503 + "<br>" 504 + "This will cause problems because you don''t see the real object." 505 + "<br>" + "Do you really want to unglue?"), 506 primitives, null); 507 } 508}