| 1 | // License: GPL. Copyright 2007 by Immanuel Scholz and others |
|---|
| 2 | package org.openstreetmap.josm.actions; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht; |
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 6 | import static org.openstreetmap.josm.tools.I18n.trn; |
|---|
| 7 | |
|---|
| 8 | import java.awt.event.ActionEvent; |
|---|
| 9 | import java.awt.event.KeyEvent; |
|---|
| 10 | import java.util.ArrayList; |
|---|
| 11 | import java.util.Collection; |
|---|
| 12 | import java.util.Collections; |
|---|
| 13 | import java.util.HashSet; |
|---|
| 14 | import java.util.LinkedList; |
|---|
| 15 | import java.util.List; |
|---|
| 16 | import java.util.Set; |
|---|
| 17 | |
|---|
| 18 | import javax.swing.JOptionPane; |
|---|
| 19 | import javax.swing.JPanel; |
|---|
| 20 | |
|---|
| 21 | import org.openstreetmap.josm.Main; |
|---|
| 22 | import org.openstreetmap.josm.command.AddCommand; |
|---|
| 23 | import org.openstreetmap.josm.command.ChangeCommand; |
|---|
| 24 | import org.openstreetmap.josm.command.Command; |
|---|
| 25 | import org.openstreetmap.josm.command.SequenceCommand; |
|---|
| 26 | import org.openstreetmap.josm.data.osm.Node; |
|---|
| 27 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 28 | import org.openstreetmap.josm.data.osm.Relation; |
|---|
| 29 | import org.openstreetmap.josm.data.osm.RelationMember; |
|---|
| 30 | import org.openstreetmap.josm.data.osm.Way; |
|---|
| 31 | import org.openstreetmap.josm.gui.MapView; |
|---|
| 32 | import org.openstreetmap.josm.tools.Shortcut; |
|---|
| 33 | |
|---|
| 34 | /** |
|---|
| 35 | * Duplicate nodes that are used by multiple ways. |
|---|
| 36 | * |
|---|
| 37 | * Resulting nodes are identical, up to their position. |
|---|
| 38 | * |
|---|
| 39 | * This is the opposite of the MergeNodesAction. |
|---|
| 40 | * |
|---|
| 41 | * If a single node is selected, it will copy that node and remove all tags from the old one |
|---|
| 42 | */ |
|---|
| 43 | |
|---|
| 44 | public class UnGlueAction extends JosmAction { |
|---|
| 45 | |
|---|
| 46 | private Node selectedNode; |
|---|
| 47 | private Way selectedWay; |
|---|
| 48 | private Set<Node> selectedNodes; |
|---|
| 49 | |
|---|
| 50 | /** |
|---|
| 51 | * Create a new UnGlueAction. |
|---|
| 52 | */ |
|---|
| 53 | public UnGlueAction() { |
|---|
| 54 | super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."), |
|---|
| 55 | Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.DIRECT), true); |
|---|
| 56 | putValue("help", ht("/Action/UnGlue")); |
|---|
| 57 | } |
|---|
| 58 | |
|---|
| 59 | /** |
|---|
| 60 | * Called when the action is executed. |
|---|
| 61 | * |
|---|
| 62 | * This method does some checking on the selection and calls the matching unGlueWay method. |
|---|
| 63 | */ |
|---|
| 64 | @Override |
|---|
| 65 | public void actionPerformed(ActionEvent e) { |
|---|
| 66 | |
|---|
| 67 | Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); |
|---|
| 68 | |
|---|
| 69 | String errMsg = null; |
|---|
| 70 | if (checkSelection(selection)) { |
|---|
| 71 | if (!checkAndConfirmOutlyingUnglue()) { |
|---|
| 72 | return; |
|---|
| 73 | } |
|---|
| 74 | int count = 0; |
|---|
| 75 | for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { |
|---|
| 76 | if (!w.isUsable() || w.getNodesCount() < 1) { |
|---|
| 77 | continue; |
|---|
| 78 | } |
|---|
| 79 | count++; |
|---|
| 80 | } |
|---|
| 81 | if (count < 2) { |
|---|
| 82 | // If there aren't enough ways, maybe the user wanted to unglue the nodes |
|---|
| 83 | // (= copy tags to a new node) |
|---|
| 84 | if (checkForUnglueNode(selection)) { |
|---|
| 85 | unglueNode(e); |
|---|
| 86 | } else { |
|---|
| 87 | errMsg = tr("This node is not glued to anything else."); |
|---|
| 88 | } |
|---|
| 89 | } else { |
|---|
| 90 | // and then do the work. |
|---|
| 91 | unglueWays(); |
|---|
| 92 | } |
|---|
| 93 | } else if (checkSelection2(selection)) { |
|---|
| 94 | if (!checkAndConfirmOutlyingUnglue()) { |
|---|
| 95 | return; |
|---|
| 96 | } |
|---|
| 97 | Set<Node> tmpNodes = new HashSet<Node>(); |
|---|
| 98 | for (Node n : selectedNodes) { |
|---|
| 99 | 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 | } |
|---|