Ticket #19111: 19111.patch
File 19111.patch, 24.9 KB (added by , 5 years ago) |
---|
-
src/org/openstreetmap/josm/actions/UnGlueAction.java
5 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import static org.openstreetmap.josm.tools.I18n.trn; 7 7 8 import java.awt.Point; 8 9 import java.awt.event.ActionEvent; 9 10 import java.awt.event.KeyEvent; 10 11 import java.util.ArrayList; 11 12 import java.util.Collection; 12 13 import java.util.Collections; 14 import java.util.HashMap; 13 15 import java.util.HashSet; 14 import java.util.LinkedList;15 16 import java.util.List; 17 import java.util.Map; 16 18 import java.util.Set; 17 19 import java.util.stream.Collectors; 18 20 19 21 import javax.swing.JOptionPane; 20 import javax.swing.JPanel;21 22 22 23 import org.openstreetmap.josm.command.AddCommand; 23 24 import org.openstreetmap.josm.command.ChangeCommand; … … 41 42 import org.openstreetmap.josm.tools.Logging; 42 43 import org.openstreetmap.josm.tools.Shortcut; 43 44 import org.openstreetmap.josm.tools.UserCancelException; 44 import org.openstreetmap.josm.tools.Utils;45 45 46 46 /** 47 * Duplicate nodes that are used by multiple ways. 47 * Duplicate nodes that are used by multiple ways or tagged nodes used by a single way 48 * or nodes which referenced more than once by a single way. 48 49 * 49 * Resulting nodes are identical, up to their position.50 *51 50 * This is the opposite of the MergeNodesAction. 52 51 * 53 * If a single node is selected, it will copy that node and remove all tags from the old one54 52 */ 55 53 public class UnGlueAction extends JosmAction { 56 54 … … 75 73 @Override 76 74 public void actionPerformed(ActionEvent e) { 77 75 try { 78 unglue( e);76 unglue(); 79 77 } catch (UserCancelException ignore) { 80 78 Logging.trace(ignore); 81 79 } finally { … … 83 81 } 84 82 } 85 83 86 protected void unglue( ActionEvent e) throws UserCancelException {84 protected void unglue() throws UserCancelException { 87 85 88 86 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected(); 89 87 90 88 String errMsg = null; 91 89 int errorTime = Notification.TIME_DEFAULT; 90 92 91 if (checkSelectionOneNodeAtMostOneWay(selection)) { 93 92 checkAndConfirmOutlyingUnglue(); 94 int count = 0; 95 for (Way w : selectedNode.getParentWays()) { 96 if (!w.isUsable() || w.getNodesCount() < 1) { 97 continue; 93 List<Way> parentWays = selectedNode.getParentWays().stream().filter(Way::isUsable).collect(Collectors.toList()); 94 95 if (parentWays.size() < 2) { 96 if (!parentWays.isEmpty()) { 97 // single way 98 Way way = selectedWay == null ? parentWays.get(0) : selectedWay; 99 boolean closedOrSelfCrossing = way.getNodes().stream().filter(n -> n == selectedNode).count() > 1; 100 101 final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary( 102 Collections.singleton(selectedNode), !selectedNode.isTagged()); 103 if (dialog != null) { 104 unglueOneNodeAtMostOneWay(way, dialog); 105 return; 106 } else if (closedOrSelfCrossing) { 107 unglueClosedOrSelfCrossingWay(way, dialog); 108 return; 109 } 98 110 } 99 count++; 100 } 101 if (count < 2) { 102 boolean selfCrossing = false; 103 if (count == 1) { 104 // First try unglue self-crossing way 105 selfCrossing = unglueSelfCrossingWay(); 106 } 107 // If there aren't enough ways, maybe the user wanted to unglue the nodes 108 // (= copy tags to a new node) 109 if (!selfCrossing) 110 if (checkForUnglueNode(selection)) { 111 unglueOneNodeAtMostOneWay(e); 112 } else { 113 errorTime = Notification.TIME_SHORT; 114 errMsg = tr("This node is not glued to anything else."); 115 } 111 errorTime = Notification.TIME_SHORT; 112 errMsg = tr("This node is not glued to anything else."); 116 113 } else { 117 114 // and then do the work. 118 115 unglueWays(); … … 119 116 } 120 117 } else if (checkSelectionOneWayAnyNodes(selection)) { 121 118 checkAndConfirmOutlyingUnglue(); 122 Set<Node> tmpNodes = new HashSet<>(); 123 for (Node n : selectedNodes) { 124 int count = 0; 125 for (Way w : n.getParentWays()) { 126 if (!w.isUsable()) { 127 continue; 128 } 129 count++; 130 } 131 if (count >= 2) { 132 tmpNodes.add(n); 133 } 134 } 135 if (tmpNodes.isEmpty()) { 119 selectedNodes.removeIf(n -> n.getParentWays().stream().filter(Way::isUsable).count() < 2); 120 if (selectedNodes.isEmpty()) { 136 121 if (selection.size() > 1) { 137 122 errMsg = tr("None of these nodes are glued to anything else."); 138 123 } else { 139 124 errMsg = tr("None of this way''s nodes are glued to anything else."); 140 125 } 126 } else if (selectedNodes.size() == 1) { 127 selectedNode = selectedNodes.iterator().next(); 128 unglueWays(); 141 129 } else { 142 130 // and then do the work. 143 selectedNodes = tmpNodes;144 131 unglueOneWayAnyNodes(); 145 132 } 146 133 } else { … … 175 162 selectedNodes = null; 176 163 } 177 164 178 static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, Collection<Command> cmds) {165 static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, List<Command> cmds) { 179 166 updateMemberships(dialog.getMemberships().orElse(null), existingNode, newNodes, cmds); 180 167 updateProperties(dialog.getTags().orElse(null), existingNode, newNodes, cmds); 181 168 } 182 169 183 private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, Collection<Command> cmds) {170 private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, List<Command> cmds) { 184 171 if (ExistingBothNew.NEW == tags) { 185 172 final Node newSelectedNode = new Node(existingNode); 186 173 newSelectedNode.removeAll(); … … 194 181 195 182 /** 196 183 * Assumes there is one tagged Node stored in selectedNode that it will try to unglue. 197 * (i.e. copy node and remove all tags from the old one. Relations will not be removed) 198 * @param e event that triggered the action 184 * (i.e. copy node and remove all tags from the old one.) 185 * @param way way to modify 186 * @param dialog 199 187 */ 200 private void unglueOneNodeAtMostOneWay( ActionEvent e) {201 final PropertiesMembershipChoiceDialog dialog;202 try {203 dialog = PropertiesMembershipChoiceDialog.showIfNecessary(Collections.singleton(selectedNode), true);204 } catch (UserCancelException ex) {205 Logging.trace(ex);206 return;188 private void unglueOneNodeAtMostOneWay(Way way, PropertiesMembershipChoiceDialog dialog) { 189 List<Command> cmds = new ArrayList<>(); 190 List<Node> newNodes = new ArrayList<>(); 191 Way modWay = modifyWay(selectedNode, way, cmds, newNodes); 192 cmds.add(new ChangeNodesCommand(way, modWay.getNodes())); 193 if (dialog != null) { 194 update(dialog, selectedNode, newNodes, cmds); 207 195 } 208 196 209 final Node unglued = new Node(selectedNode, true); 210 boolean moveSelectedNode = false; 211 212 List<Command> cmds = new LinkedList<>(); 213 cmds.add(new AddCommand(selectedNode.getDataSet(), unglued)); 214 if (dialog != null && ExistingBothNew.NEW == dialog.getTags().orElse(null)) { 215 // unglued node gets the ID and history, thus replace way node with a fresh one 216 final Way way = selectedNode.getParentWays().get(0); 217 final List<Node> newWayNodes = way.getNodes(); 218 newWayNodes.replaceAll(n -> selectedNode.equals(n) ? unglued : n); 219 cmds.add(new ChangeNodesCommand(way, newWayNodes)); 220 updateMemberships(dialog.getMemberships().map(ExistingBothNew::opposite).orElse(null), 221 selectedNode, Collections.singletonList(unglued), cmds); 222 updateProperties(dialog.getTags().map(ExistingBothNew::opposite).orElse(null), 223 selectedNode, Collections.singletonList(unglued), cmds); 224 moveSelectedNode = true; 225 } else if (dialog != null) { 226 update(dialog, selectedNode, Collections.singletonList(unglued), cmds); 227 } 228 229 // If this wasn't called from menu, place it where the cursor is/was 197 // Place the selected node where the cursor is/was 230 198 MapView mv = MainApplication.getMap().mapView; 231 if (e.getSource() instanceof JPanel) { 232 final LatLon latLon = mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY()); 233 if (moveSelectedNode) { 234 cmds.add(new MoveCommand(selectedNode, latLon)); 235 } else { 236 unglued.setCoor(latLon); 237 } 199 Point currMousePos = mv.getMousePosition(); 200 if (currMousePos != null) { 201 final LatLon latLon = mv.getLatLon(currMousePos.getX(), currMousePos.getY()); 202 cmds.add(new MoveCommand(selectedNode, latLon)); 203 } else { 204 cmds.add(new MoveCommand(selectedNode, 0, 5)); 238 205 } 239 240 206 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Unglued Node"), cmds)); 241 getLayerManager().getEditDataSet().setSelected(moveSelectedNode ? selectedNode : unglued); 242 mv.repaint(); 207 getLayerManager().getEditDataSet().setSelected(selectedNode); 243 208 } 244 209 245 210 /** 246 * Checks if selection is suitable for ungluing. This is the case when there's a single,247 * tagged node selected that's part of at least one way (ungluing an unconnected node does248 * not make sense. Due to the call order in actionPerformed, this is only called when the249 * node is only part of one or less ways.250 *251 * @param selection The selection to check against252 * @return {@code true} if selection is suitable253 */254 private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {255 if (selection.size() != 1)256 return false;257 OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];258 if (!(n instanceof Node))259 return false;260 if (((Node) n).getParentWays().isEmpty())261 return false;262 263 selectedNode = (Node) n;264 return selectedNode.isTagged();265 }266 267 /**268 211 * Checks if the selection consists of something we can work with. 269 212 * Checks only if the number and type of items selected looks good. 270 213 * 271 * If this method returns "true", selectedNode and selectedWay will be set.214 * If this method returns "true", selectedNode will be set, selectedWay might be set 272 215 * 273 216 * Returns true if either one node is selected or one node and one 274 217 * way are selected and the node is part of the way. … … 361 304 */ 362 305 private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { 363 306 // clone the node for the way 364 Node newNode = new Node(originalNode, true /* clear OSM ID */);307 Node newNode = cloneNode(originalNode, cmds); 365 308 newNodes.add(newNode); 366 cmds.add(new AddCommand(originalNode.getDataSet(), newNode));367 309 368 List<Node> nn = new ArrayList<>(); 369 for (Node pushNode : w.getNodes()) { 370 if (originalNode == pushNode) { 371 pushNode = newNode; 372 } 373 nn.add(pushNode); 374 } 310 List<Node> nn = new ArrayList<>(w.getNodes()); 311 nn.replaceAll(n -> n == originalNode ? newNode : n); 375 312 Way newWay = new Way(w); 376 313 newWay.setNodes(nn); 377 314 … … 378 315 return newWay; 379 316 } 380 317 318 private static Node cloneNode(Node originalNode, List<Command> cmds) { 319 Node newNode = new Node(originalNode, true /* clear OSM ID */); 320 cmds.add(new AddCommand(originalNode.getDataSet(), newNode)); 321 return newNode; 322 } 323 381 324 /** 382 325 * put all newNodes into the same relation(s) that originalNode is in 383 326 * @param memberships where the memberships should be places … … 385 328 * @param cmds List of commands that will contain the new "change relation" commands 386 329 * @param newNodes List of nodes that contain the new node 387 330 */ 388 private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, Collection<Command> cmds) {331 private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, List<Command> cmds) { 389 332 if (memberships == null || ExistingBothNew.OLD == memberships) { 390 333 return; 391 334 } … … 421 364 * dupe a single node into as many nodes as there are ways using it, OR 422 365 * 423 366 * dupe a single node once, and put the copy on the selected way 367 * @throws UserCancelException if user cancels choice 424 368 */ 425 private void unglueWays() { 426 final PropertiesMembershipChoiceDialog dialog; 427 try { 428 dialog = PropertiesMembershipChoiceDialog.showIfNecessary(Collections.singleton(selectedNode), false); 429 } catch (UserCancelException e) { 430 Logging.trace(e); 431 return; 432 } 433 434 List<Command> cmds = new LinkedList<>(); 435 List<Node> newNodes = new LinkedList<>(); 369 private void unglueWays() throws UserCancelException { 370 final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog 371 .showIfNecessary(Collections.singleton(selectedNode), false); 372 List<Command> cmds = new ArrayList<>(); 373 List<Node> newNodes = new ArrayList<>(); 374 List<Way> parentWays; 436 375 if (selectedWay == null) { 437 LinkedList<Way> parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable) 438 .collect(Collectors.toCollection(LinkedList::new)); 376 parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable).collect(Collectors.toList()); 439 377 // see #5452 and #18670 440 378 parentWays.sort((o1, o2) -> { 441 379 int d = Boolean.compare(!o1.isNew() && !o1.isModified(), !o2.isNew() && !o2.isModified()); … … 448 386 return d; 449 387 }); 450 388 // first way should not be changed, preferring older ways and those with fewer parents 451 parentWays.removeFirst(); 452 453 Set<Way> warnParents = new HashSet<>(); 454 for (Way w : parentWays) { 455 if (w.isFirstLastNode(selectedNode)) 456 warnParents.add(w); 457 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); 458 } 459 notifyWayPartOfRelation(warnParents); 389 parentWays.remove(0); 460 390 } else { 461 Way modWay = modifyWay(selectedNode, selectedWay, cmds, newNodes); 462 addCheckedChangeNodesCmd(cmds, selectedWay, modWay.getNodes()); 391 parentWays = Collections.singletonList(selectedWay); 463 392 } 393 Set<Way> warnParents = new HashSet<>(); 394 for (Way w : parentWays) { 395 if (w.isFirstLastNode(selectedNode)) 396 warnParents.add(w); 397 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); 398 } 464 399 465 400 if (dialog != null) { 466 401 update(dialog, selectedNode, newNodes, cmds); 467 402 } 403 notifyWayPartOfRelation(warnParents); 468 404 469 405 execCommands(cmds, newNodes); 470 406 } … … 483 419 484 420 /** 485 421 * Duplicates a node used several times by the same way. See #9896. 422 * A closed way will be "opened" when the closing node is unglued. 423 * @param way way to modify 424 * @param dialog user dialog, might be null 486 425 * @return true if action is OK false if there is nothing to do 487 426 */ 488 private boolean unglue SelfCrossingWay() {427 private boolean unglueClosedOrSelfCrossingWay(Way way, PropertiesMembershipChoiceDialog dialog) { 489 428 // According to previous check, only one valid way through that node 490 Way way = null; 491 for (Way w: selectedNode.getParentWays()) { 492 if (w.isUsable() && w.getNodesCount() >= 1) { 493 way = w; 494 } 495 } 496 if (way == null) { 497 return false; 498 } 499 List<Command> cmds = new LinkedList<>(); 429 List<Command> cmds = new ArrayList<>(); 500 430 List<Node> oldNodes = way.getNodes(); 501 431 List<Node> newNodes = new ArrayList<>(oldNodes.size()); 502 432 List<Node> addNodes = new ArrayList<>(); 503 boolean seen = false;433 int count = 0; 504 434 for (Node n: oldNodes) { 505 if (n == selectedNode) { 506 if (seen) { 507 Node newNode = new Node(n, true /* clear OSM ID */); 508 cmds.add(new AddCommand(selectedNode.getDataSet(), newNode)); 509 newNodes.add(newNode); 510 addNodes.add(newNode); 511 } else { 512 newNodes.add(n); 513 seen = true; 514 } 515 } else { 516 newNodes.add(n); 435 if (n == selectedNode && count++ > 0) { 436 n = cloneNode(selectedNode, cmds); 437 addNodes.add(n); 517 438 } 439 newNodes.add(n); 518 440 } 519 441 if (addNodes.isEmpty()) { 520 442 // selectedNode doesn't need unglue 521 443 return false; 522 444 } 445 if (dialog != null) { 446 update(dialog, selectedNode, addNodes, cmds); 447 } 523 448 addCheckedChangeNodesCmd(cmds, way, newNodes); 524 try { 525 final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary( 526 Collections.singleton(selectedNode), false); 527 if (dialog != null) { 528 update(dialog, selectedNode, addNodes, cmds); 529 } 530 execCommands(cmds, addNodes); 531 return true; 532 } catch (UserCancelException ignore) { 533 Logging.trace(ignore); 534 } 535 return false; 449 execCommands(cmds, addNodes); 450 return true; 536 451 } 537 452 538 453 /** 539 454 * dupe all nodes that are selected, and put the copies on the selected way 455 * @throws UserCancelException 540 456 * 541 457 */ 542 private void unglueOneWayAnyNodes() { 543 Way tmpWay = selectedWay; 458 private void unglueOneWayAnyNodes() throws UserCancelException { 459 final PropertiesMembershipChoiceDialog dialog = 460 PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false); 544 461 545 final PropertiesMembershipChoiceDialog dialog; 546 try { 547 dialog = PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false); 548 } catch (UserCancelException e) { 549 Logging.trace(e); 550 return; 551 } 462 Map<Node, Node> replaced = new HashMap<>(); 463 List<Command> cmds = new ArrayList<>(); 552 464 553 List<Command> cmds = new LinkedList<>(); 554 List<Node> allNewNodes = new LinkedList<>(); 555 for (Node n : selectedNodes) { 556 List<Node> newNodes = new LinkedList<>(); 557 tmpWay = modifyWay(n, tmpWay, cmds, newNodes); 558 if (dialog != null) { 559 update(dialog, n, newNodes, cmds); 560 } 561 allNewNodes.addAll(newNodes); 465 selectedNodes.forEach(n -> replaced.put(n, cloneNode(n, cmds))); 466 List<Node> modNodes = selectedWay.getNodes().stream().map(n -> replaced.getOrDefault(n, n)) 467 .collect(Collectors.toList()); 468 469 // only one changeCommand for a way, else garbage will happen 470 addCheckedChangeNodesCmd(cmds, selectedWay, modNodes); 471 if (dialog != null) { 472 replaced.forEach((k,v) -> update(dialog, k, Collections.singletonList(v), cmds)); 562 473 } 563 // only one changeCommand for a way, else garbage will happen564 addCheckedChangeNodesCmd(cmds, selectedWay, tmpWay.getNodes());565 474 566 475 UndoRedoHandler.getInstance().add(new SequenceCommand( 567 476 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", 568 selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));569 getLayerManager().getEditDataSet().setSelected( allNewNodes);477 selectedNodes.size(), selectedNodes.size(), 2 * selectedNodes.size()), cmds)); 478 getLayerManager().getEditDataSet().setSelected(replaced.values()); 570 479 } 571 480 572 private voidaddCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) {573 boolean relationCheck = w.firstNode() != nodes.get(0) || w.lastNode() != nodes.get(nodes.size() - 1);481 private boolean addCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) { 482 boolean relationCheck = !calcAffectedRelations(Collections.singleton(w)).isEmpty(); 574 483 cmds.add(new ChangeNodesCommand(w, nodes)); 575 484 if (relationCheck) { 576 485 notifyWayPartOfRelation(Collections.singleton(w)); 577 486 } 487 return relationCheck; 578 488 } 579 489 580 490 @Override … … 611 521 } 612 522 613 523 protected void notifyWayPartOfRelation(final Collection<Way> ways) { 614 final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode); 615 final Set<String> affectedRelations = new HashSet<>(); 616 for (Relation r: OsmPrimitive.getParentRelations(ways)) { 617 if (!r.isUsable()) 618 continue; 619 // see #18670: suppress notification when well known restriction types are not affected 620 if (r.hasTag("type", "restriction", "connectivity", "destination_sign") && !r.hasIncompleteMembers()) { 621 int count = 0; 622 for (RelationMember rm : r.getMembers()) { 623 if (rm.isNode() && affectedNodes.contains(rm.getNode())) 624 count++; 625 if (rm.isWay() && ways.contains(rm.getWay())) { 626 count++; 627 if ("via".equals(rm.getRole())) { 628 count++; 629 } 630 } 631 } 632 if (count < 2) 633 continue; 634 } 635 affectedRelations.add(r.getDisplayName(DefaultNameFormatter.getInstance())); 636 } 524 Set<Relation> affectedRelations = calcAffectedRelations(ways); 637 525 if (affectedRelations.isEmpty()) { 638 526 return; 639 527 } 640 641 528 final int size = affectedRelations.size(); 642 529 final String msg1 = trn("Unglueing possibly affected {0} relation: {1}", "Unglueing possibly affected {0} relations: {1}", 643 size, size, Utils.joinAsHtmlUnorderedList(affectedRelations));530 size, size, DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(affectedRelations, 20)); 644 531 final String msg2 = trn("Ensure that the relation has not been broken!", "Ensure that the relations have not been broken!", 645 532 size); 646 new Notification( "<html>" +msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show();533 new Notification(msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show(); 647 534 } 535 536 protected Set<Relation> calcAffectedRelations(final Collection<Way> ways) { 537 final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode); 538 return OsmPrimitive.getParentRelations(ways) 539 .stream().filter(r -> isRelationAffected(r, affectedNodes, ways)) 540 .collect(Collectors.toSet()); 541 } 542 543 private static boolean isRelationAffected(Relation r, Set<Node> affectedNodes, Collection<Way> ways) { 544 if (!r.isUsable()) 545 return false; 546 // see #18670: suppress notification when well known restriction types are not affected 547 if (!r.hasTag("type", "restriction", "connectivity", "destination_sign") || r.hasIncompleteMembers()) 548 return true; 549 int count = 0; 550 for (RelationMember rm : r.getMembers()) { 551 if (rm.isNode() && affectedNodes.contains(rm.getNode())) 552 count++; 553 if (rm.isWay() && ways.contains(rm.getWay())) { 554 count++; 555 if ("via".equals(rm.getRole())) { 556 count++; 557 } 558 } 559 } 560 return count >= 2; 561 } 648 562 }