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