[858] | 1 | // License: GPL. Copyright 2007 by Immanuel Scholz and others
|
---|
| 2 | package org.openstreetmap.josm.actions;
|
---|
| 3 |
|
---|
[2410] | 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
[858] | 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
[2842] | 6 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
[858] | 7 |
|
---|
| 8 | import java.awt.event.ActionEvent;
|
---|
| 9 | import java.awt.event.KeyEvent;
|
---|
| 10 | import java.util.ArrayList;
|
---|
| 11 | import java.util.Collection;
|
---|
[1429] | 12 | import java.util.Collections;
|
---|
[858] | 13 | import java.util.HashSet;
|
---|
| 14 | import java.util.LinkedList;
|
---|
| 15 | import java.util.List;
|
---|
| 16 |
|
---|
| 17 | import javax.swing.JOptionPane;
|
---|
[1429] | 18 | import javax.swing.JPanel;
|
---|
[858] | 19 |
|
---|
| 20 | import org.openstreetmap.josm.Main;
|
---|
| 21 | import org.openstreetmap.josm.command.AddCommand;
|
---|
| 22 | import org.openstreetmap.josm.command.ChangeCommand;
|
---|
| 23 | import org.openstreetmap.josm.command.Command;
|
---|
| 24 | import org.openstreetmap.josm.command.SequenceCommand;
|
---|
| 25 | import org.openstreetmap.josm.data.osm.Node;
|
---|
| 26 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
| 27 | import org.openstreetmap.josm.data.osm.Relation;
|
---|
| 28 | import org.openstreetmap.josm.data.osm.RelationMember;
|
---|
| 29 | import org.openstreetmap.josm.data.osm.Way;
|
---|
[1429] | 30 | import org.openstreetmap.josm.gui.MapView;
|
---|
[1084] | 31 | import org.openstreetmap.josm.tools.Shortcut;
|
---|
[858] | 32 |
|
---|
| 33 | /**
|
---|
[1024] | 34 | * Duplicate nodes that are used by multiple ways.
|
---|
[858] | 35 | *
|
---|
| 36 | * Resulting nodes are identical, up to their position.
|
---|
| 37 | *
|
---|
| 38 | * This is the opposite of the MergeNodesAction.
|
---|
[1677] | 39 | *
|
---|
[1429] | 40 | * If a single node is selected, it will copy that node and remove all tags from the old one
|
---|
[858] | 41 | */
|
---|
| 42 |
|
---|
[1820] | 43 | public class UnGlueAction extends JosmAction {
|
---|
[858] | 44 |
|
---|
[1169] | 45 | private Node selectedNode;
|
---|
| 46 | private Way selectedWay;
|
---|
| 47 | private ArrayList<Node> selectedNodes;
|
---|
[858] | 48 |
|
---|
[1169] | 49 | /**
|
---|
| 50 | * Create a new UnGlueAction.
|
---|
| 51 | */
|
---|
| 52 | public UnGlueAction() {
|
---|
| 53 | super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."),
|
---|
[1814] | 54 | Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.GROUP_EDIT), true);
|
---|
[2323] | 55 | putValue("help", ht("/Action/UnGlue"));
|
---|
[1169] | 56 | }
|
---|
[858] | 57 |
|
---|
[1169] | 58 | /**
|
---|
| 59 | * Called when the action is executed.
|
---|
| 60 | *
|
---|
| 61 | * This method does some checking on the selection and calls the matching unGlueWay method.
|
---|
| 62 | */
|
---|
| 63 | public void actionPerformed(ActionEvent e) {
|
---|
[858] | 64 |
|
---|
[1814] | 65 | Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
|
---|
[1677] | 66 |
|
---|
[1429] | 67 | String errMsg = null;
|
---|
[1169] | 68 | if (checkSelection(selection)) {
|
---|
| 69 | int count = 0;
|
---|
[2412] | 70 | for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
|
---|
[2120] | 71 | if (!w.isUsable() || w.getNodesCount() < 1) {
|
---|
[1814] | 72 | continue;
|
---|
| 73 | }
|
---|
[1169] | 74 | count++;
|
---|
| 75 | }
|
---|
| 76 | if (count < 2) {
|
---|
[1429] | 77 | // If there aren't enough ways, maybe the user wanted to unglue the nodes
|
---|
| 78 | // (= copy tags to a new node)
|
---|
[2381] | 79 | if (checkForUnglueNode(selection)) {
|
---|
[1429] | 80 | unglueNode(e);
|
---|
[1814] | 81 | } else {
|
---|
[1429] | 82 | errMsg = tr("This node is not glued to anything else.");
|
---|
[1814] | 83 | }
|
---|
[1169] | 84 | } else {
|
---|
| 85 | // and then do the work.
|
---|
| 86 | unglueWays();
|
---|
| 87 | }
|
---|
| 88 | } else if (checkSelection2(selection)) {
|
---|
| 89 | ArrayList<Node> tmpNodes = new ArrayList<Node>();
|
---|
| 90 | for (Node n : selectedNodes) {
|
---|
| 91 | int count = 0;
|
---|
[2412] | 92 | for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
|
---|
[2748] | 93 | if (!w.isUsable()) {
|
---|
[1814] | 94 | continue;
|
---|
[2748] | 95 | }
|
---|
[1169] | 96 | count++;
|
---|
| 97 | }
|
---|
| 98 | if (count >= 2) {
|
---|
| 99 | tmpNodes.add(n);
|
---|
| 100 | }
|
---|
| 101 | }
|
---|
| 102 | if (tmpNodes.size() < 1) {
|
---|
| 103 | if (selection.size() > 1) {
|
---|
[1429] | 104 | errMsg = tr("None of these nodes are glued to anything else.");
|
---|
[1169] | 105 | } else {
|
---|
[2842] | 106 | errMsg = tr("None of this way''s nodes are glued to anything else.");
|
---|
[1169] | 107 | }
|
---|
| 108 | } else {
|
---|
| 109 | // and then do the work.
|
---|
| 110 | selectedNodes = tmpNodes;
|
---|
| 111 | unglueWays2();
|
---|
| 112 | }
|
---|
| 113 | } else {
|
---|
[1429] | 114 | errMsg =
|
---|
[1169] | 115 | tr("The current selection cannot be used for unglueing.")+"\n"+
|
---|
| 116 | "\n"+
|
---|
| 117 | tr("Select either:")+"\n"+
|
---|
[1429] | 118 | tr("* One tagged node, or")+"\n"+
|
---|
[1169] | 119 | tr("* One node that is used by more than one way, or")+"\n"+
|
---|
| 120 | tr("* One node that is used by more than one way and one of those ways, or")+"\n"+
|
---|
| 121 | tr("* One way that has one or more nodes that are used by more than one way, or")+"\n"+
|
---|
| 122 | tr("* One way and one or more of its nodes that are used by more than one way.")+"\n"+
|
---|
| 123 | "\n"+
|
---|
| 124 | tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+
|
---|
[1814] | 125 | "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+
|
---|
| 126 | "own copy and all nodes will be selected.");
|
---|
[1169] | 127 | }
|
---|
[1677] | 128 |
|
---|
[1814] | 129 | if(errMsg != null) {
|
---|
[2017] | 130 | JOptionPane.showMessageDialog(
|
---|
[1857] | 131 | Main.parent,
|
---|
| 132 | errMsg,
|
---|
| 133 | tr("Error"),
|
---|
| 134 | JOptionPane.ERROR_MESSAGE);
|
---|
[1814] | 135 | }
|
---|
[1677] | 136 |
|
---|
[1169] | 137 | selectedNode = null;
|
---|
| 138 | selectedWay = null;
|
---|
| 139 | selectedNodes = null;
|
---|
| 140 | }
|
---|
[1677] | 141 |
|
---|
[1429] | 142 | /**
|
---|
| 143 | * Assumes there is one tagged Node stored in selectedNode that it will try to unglue
|
---|
| 144 | * (= copy node and remove all tags from the old one. Relations will not be removed)
|
---|
| 145 | */
|
---|
| 146 | private void unglueNode(ActionEvent e) {
|
---|
| 147 | LinkedList<Command> cmds = new LinkedList<Command>();
|
---|
[858] | 148 |
|
---|
[1429] | 149 | Node c = new Node(selectedNode);
|
---|
[1924] | 150 | c.removeAll();
|
---|
[2264] | 151 | getCurrentDataSet().clearSelection(c);
|
---|
[1429] | 152 | cmds.add(new ChangeCommand(selectedNode, c));
|
---|
[1677] | 153 |
|
---|
[2410] | 154 | Node n = new Node(selectedNode, true);
|
---|
[1677] | 155 |
|
---|
[1429] | 156 | // If this wasn't called from menu, place it where the cursor is/was
|
---|
| 157 | if(e.getSource() instanceof JPanel) {
|
---|
| 158 | MapView mv = Main.map.mapView;
|
---|
[1722] | 159 | n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY()));
|
---|
[1429] | 160 | }
|
---|
[1677] | 161 |
|
---|
[1429] | 162 | cmds.add(new AddCommand(n));
|
---|
[1677] | 163 |
|
---|
[1429] | 164 | fixRelations(selectedNode, cmds, Collections.singletonList(n));
|
---|
[1677] | 165 |
|
---|
[1429] | 166 | Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds));
|
---|
[1814] | 167 | getCurrentDataSet().setSelected(n);
|
---|
[1429] | 168 | Main.map.mapView.repaint();
|
---|
| 169 | }
|
---|
[1677] | 170 |
|
---|
[1169] | 171 | /**
|
---|
[1429] | 172 | * Checks if selection is suitable for ungluing. This is the case when there's a single,
|
---|
| 173 | * tagged node selected that's part of at least one way (ungluing an unconnected node does
|
---|
| 174 | * not make sense. Due to the call order in actionPerformed, this is only called when the
|
---|
[1677] | 175 | * node is only part of one or less ways.
|
---|
| 176 | *
|
---|
[1429] | 177 | * @param The selection to check against
|
---|
| 178 | * @return Selection is suitable
|
---|
| 179 | */
|
---|
| 180 | private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {
|
---|
[2381] | 181 | if (selection.size() != 1)
|
---|
[1429] | 182 | return false;
|
---|
| 183 | OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];
|
---|
[2381] | 184 | if (!(n instanceof Node))
|
---|
[1429] | 185 | return false;
|
---|
[2412] | 186 | if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty())
|
---|
[1429] | 187 | return false;
|
---|
[1677] | 188 |
|
---|
[2381] | 189 | selectedNode = (Node) n;
|
---|
| 190 | return selectedNode.isTagged();
|
---|
[1429] | 191 | }
|
---|
| 192 |
|
---|
| 193 | /**
|
---|
[1169] | 194 | * Checks if the selection consists of something we can work with.
|
---|
[3538] | 195 | * Checks only if the number and type of items selected looks good.
|
---|
[1169] | 196 | *
|
---|
| 197 | * If this method returns "true", selectedNode and selectedWay will
|
---|
| 198 | * be set.
|
---|
| 199 | *
|
---|
| 200 | * Returns true if either one node is selected or one node and one
|
---|
| 201 | * way are selected and the node is part of the way.
|
---|
| 202 | *
|
---|
| 203 | * The way will be put into the object variable "selectedWay", the
|
---|
| 204 | * node into "selectedNode".
|
---|
| 205 | */
|
---|
| 206 | private boolean checkSelection(Collection<? extends OsmPrimitive> selection) {
|
---|
[1023] | 207 |
|
---|
[1169] | 208 | int size = selection.size();
|
---|
| 209 | if (size < 1 || size > 2)
|
---|
| 210 | return false;
|
---|
[1023] | 211 |
|
---|
[1169] | 212 | selectedNode = null;
|
---|
| 213 | selectedWay = null;
|
---|
[1023] | 214 |
|
---|
[1169] | 215 | for (OsmPrimitive p : selection) {
|
---|
| 216 | if (p instanceof Node) {
|
---|
| 217 | selectedNode = (Node) p;
|
---|
| 218 | if (size == 1 || selectedWay != null)
|
---|
[1912] | 219 | return size == 1 || selectedWay.containsNode(selectedNode);
|
---|
[1169] | 220 | } else if (p instanceof Way) {
|
---|
| 221 | selectedWay = (Way) p;
|
---|
| 222 | if (size == 2 && selectedNode != null)
|
---|
[1912] | 223 | return selectedWay.containsNode(selectedNode);
|
---|
[1169] | 224 | }
|
---|
| 225 | }
|
---|
[1023] | 226 |
|
---|
[1169] | 227 | return false;
|
---|
| 228 | }
|
---|
[858] | 229 |
|
---|
[1169] | 230 | /**
|
---|
| 231 | * Checks if the selection consists of something we can work with.
|
---|
[3538] | 232 | * Checks only if the number and type of items selected looks good.
|
---|
[1169] | 233 | *
|
---|
| 234 | * Returns true if one way and any number of nodes that are part of
|
---|
| 235 | * that way are selected. Note: "any" can be none, then all nodes of
|
---|
| 236 | * the way are used.
|
---|
| 237 | *
|
---|
| 238 | * The way will be put into the object variable "selectedWay", the
|
---|
| 239 | * nodes into "selectedNodes".
|
---|
| 240 | */
|
---|
| 241 | private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) {
|
---|
| 242 | if (selection.size() < 1)
|
---|
| 243 | return false;
|
---|
[1024] | 244 |
|
---|
[1169] | 245 | selectedWay = null;
|
---|
| 246 | for (OsmPrimitive p : selection) {
|
---|
| 247 | if (p instanceof Way) {
|
---|
[1814] | 248 | if (selectedWay != null)
|
---|
[1169] | 249 | return false;
|
---|
| 250 | selectedWay = (Way) p;
|
---|
| 251 | }
|
---|
| 252 | }
|
---|
[1814] | 253 | if (selectedWay == null)
|
---|
[1169] | 254 | return false;
|
---|
[1024] | 255 |
|
---|
[1169] | 256 | selectedNodes = new ArrayList<Node>();
|
---|
| 257 | for (OsmPrimitive p : selection) {
|
---|
| 258 | if (p instanceof Node) {
|
---|
| 259 | Node n = (Node) p;
|
---|
[1912] | 260 | if (!selectedWay.containsNode(n))
|
---|
[1169] | 261 | return false;
|
---|
| 262 | selectedNodes.add(n);
|
---|
| 263 | }
|
---|
| 264 | }
|
---|
[1024] | 265 |
|
---|
[1169] | 266 | if (selectedNodes.size() < 1) {
|
---|
[1862] | 267 | selectedNodes.addAll(selectedWay.getNodes());
|
---|
[1169] | 268 | }
|
---|
[1024] | 269 |
|
---|
[1169] | 270 | return true;
|
---|
| 271 | }
|
---|
[1024] | 272 |
|
---|
[1169] | 273 | /**
|
---|
| 274 | * dupe the given node of the given way
|
---|
| 275 | *
|
---|
[3241] | 276 | * assume that OrginalNode is in the way
|
---|
| 277 | *
|
---|
[1169] | 278 | * -> the new node will be put into the parameter newNodes.
|
---|
| 279 | * -> the add-node command will be put into the parameter cmds.
|
---|
| 280 | * -> the changed way will be returned and must be put into cmds by the caller!
|
---|
| 281 | */
|
---|
| 282 | private Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
|
---|
[3241] | 283 | // clone the node for the way
|
---|
| 284 | Node newNode = new Node(originalNode, true /* clear OSM ID */);
|
---|
| 285 | newNodes.add(newNode);
|
---|
| 286 | cmds.add(new AddCommand(newNode));
|
---|
| 287 |
|
---|
[1169] | 288 | ArrayList<Node> nn = new ArrayList<Node>();
|
---|
[1862] | 289 | for (Node pushNode : w.getNodes()) {
|
---|
[1169] | 290 | if (originalNode == pushNode) {
|
---|
[3241] | 291 | pushNode = newNode;
|
---|
[1169] | 292 | }
|
---|
| 293 | nn.add(pushNode);
|
---|
| 294 | }
|
---|
| 295 | Way newWay = new Way(w);
|
---|
[1862] | 296 | newWay.setNodes(nn);
|
---|
[858] | 297 |
|
---|
[1169] | 298 | return newWay;
|
---|
| 299 | }
|
---|
[1023] | 300 |
|
---|
[1169] | 301 | /**
|
---|
| 302 | * put all newNodes into the same relation(s) that originalNode is in
|
---|
| 303 | */
|
---|
| 304 | private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) {
|
---|
| 305 | // modify all relations containing the node
|
---|
| 306 | Relation newRel = null;
|
---|
| 307 | HashSet<String> rolesToReAdd = null;
|
---|
[2412] | 308 | for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) {
|
---|
[2748] | 309 | if (r.isDeleted()) {
|
---|
[1814] | 310 | continue;
|
---|
[2748] | 311 | }
|
---|
[1169] | 312 | newRel = null;
|
---|
| 313 | rolesToReAdd = null;
|
---|
[1925] | 314 | for (RelationMember rm : r.getMembers()) {
|
---|
[1938] | 315 | if (rm.isNode()) {
|
---|
| 316 | if (rm.getMember() == originalNode) {
|
---|
[1169] | 317 | if (newRel == null) {
|
---|
| 318 | newRel = new Relation(r);
|
---|
| 319 | rolesToReAdd = new HashSet<String>();
|
---|
| 320 | }
|
---|
[1930] | 321 | rolesToReAdd.add(rm.getRole());
|
---|
[1169] | 322 | }
|
---|
| 323 | }
|
---|
| 324 | }
|
---|
| 325 | if (newRel != null) {
|
---|
| 326 | for (Node n : newNodes) {
|
---|
| 327 | for (String role : rolesToReAdd) {
|
---|
[2070] | 328 | newRel.addMember(new RelationMember(role, n));
|
---|
[1169] | 329 | }
|
---|
| 330 | }
|
---|
| 331 | cmds.add(new ChangeCommand(r, newRel));
|
---|
| 332 | }
|
---|
| 333 | }
|
---|
| 334 | }
|
---|
[858] | 335 |
|
---|
[1169] | 336 | /**
|
---|
| 337 | * dupe a single node into as many nodes as there are ways using it, OR
|
---|
| 338 | *
|
---|
| 339 | * dupe a single node once, and put the copy on the selected way
|
---|
| 340 | */
|
---|
| 341 | private void unglueWays() {
|
---|
| 342 | LinkedList<Command> cmds = new LinkedList<Command>();
|
---|
[3538] | 343 | LinkedList<Node> newNodes = new LinkedList<Node>();
|
---|
[1024] | 344 |
|
---|
[1169] | 345 | if (selectedWay == null) {
|
---|
[3538] | 346 | Way wayWithSelectedNode = null;
|
---|
| 347 | LinkedList<Way> parentWays = new LinkedList<Way>();
|
---|
| 348 | for (OsmPrimitive osm : selectedNode.getReferrers()) {
|
---|
| 349 | if (osm.isUsable() && osm instanceof Way) {
|
---|
| 350 | Way w = (Way) osm;
|
---|
| 351 | if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) {
|
---|
| 352 | wayWithSelectedNode = w;
|
---|
| 353 | } else {
|
---|
| 354 | parentWays.add(w);
|
---|
| 355 | }
|
---|
[1814] | 356 | }
|
---|
[1169] | 357 | }
|
---|
[3538] | 358 | if (wayWithSelectedNode == null) {
|
---|
| 359 | wayWithSelectedNode = parentWays.removeFirst();
|
---|
| 360 | }
|
---|
| 361 | for (Way w : parentWays) {
|
---|
| 362 | cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
|
---|
| 363 | }
|
---|
[1169] | 364 | } else {
|
---|
| 365 | cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes)));
|
---|
| 366 | }
|
---|
[1024] | 367 |
|
---|
[1169] | 368 | fixRelations(selectedNode, cmds, newNodes);
|
---|
[1024] | 369 |
|
---|
[1169] | 370 | Main.main.undoRedo.add(new SequenceCommand(tr("Dupe into {0} nodes", newNodes.size()+1), cmds));
|
---|
[3538] | 371 | // select one of the new nodes
|
---|
| 372 | getCurrentDataSet().setSelected(newNodes.getFirst());
|
---|
[1169] | 373 | }
|
---|
[858] | 374 |
|
---|
[1169] | 375 | /**
|
---|
| 376 | * dupe all nodes that are selected, and put the copies on the selected way
|
---|
| 377 | *
|
---|
| 378 | */
|
---|
| 379 | private void unglueWays2() {
|
---|
| 380 | LinkedList<Command> cmds = new LinkedList<Command>();
|
---|
| 381 | List<Node> allNewNodes = new LinkedList<Node>();
|
---|
| 382 | Way tmpWay = selectedWay;
|
---|
[1024] | 383 |
|
---|
[1169] | 384 | for (Node n : selectedNodes) {
|
---|
| 385 | List<Node> newNodes = new LinkedList<Node>();
|
---|
| 386 | tmpWay = modifyWay(n, tmpWay, cmds, newNodes);
|
---|
| 387 | fixRelations(n, cmds, newNodes);
|
---|
| 388 | allNewNodes.addAll(newNodes);
|
---|
| 389 | }
|
---|
| 390 | cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen
|
---|
[1024] | 391 |
|
---|
[2842] | 392 | Main.main.undoRedo.add(new SequenceCommand(
|
---|
| 393 | trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));
|
---|
[1814] | 394 | getCurrentDataSet().setSelected(allNewNodes);
|
---|
[1169] | 395 | }
|
---|
[1820] | 396 |
|
---|
| 397 | @Override
|
---|
| 398 | protected void updateEnabledState() {
|
---|
[2256] | 399 | if (getCurrentDataSet() == null) {
|
---|
| 400 | setEnabled(false);
|
---|
| 401 | } else {
|
---|
| 402 | updateEnabledState(getCurrentDataSet().getSelected());
|
---|
| 403 | }
|
---|
[1820] | 404 | }
|
---|
[2256] | 405 |
|
---|
| 406 | @Override
|
---|
| 407 | protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
|
---|
| 408 | setEnabled(selection != null && !selection.isEmpty());
|
---|
| 409 | }
|
---|
[858] | 410 | }
|
---|