Changeset 16362 in josm for trunk/src/org
- Timestamp:
- 2020-04-20T09:36:29+02:00 (4 years ago)
- Location:
- trunk/src/org/openstreetmap/josm
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/actions/UnGlueAction.java
r16317 r16362 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; … … 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; … … 27 28 import org.openstreetmap.josm.command.SequenceCommand; 28 29 import org.openstreetmap.josm.data.UndoRedoHandler; 29 import org.openstreetmap.josm.data.coor.LatLon;30 30 import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 31 31 import org.openstreetmap.josm.data.osm.Node; … … 42 42 import org.openstreetmap.josm.tools.Shortcut; 43 43 import org.openstreetmap.josm.tools.UserCancelException; 44 import org.openstreetmap.josm.tools.Utils;45 44 46 45 /** 47 * Duplicate nodes that are used by multiple ways. 48 * 49 * Resulting nodes are identical, up to their position. 46 * Duplicate nodes that are used by multiple ways or tagged nodes used by a single way 47 * or nodes which referenced more than once by a single way. 50 48 * 51 49 * This is the opposite of the MergeNodesAction. 52 50 * 53 * If a single node is selected, it will copy that node and remove all tags from the old one54 51 */ 55 52 public class UnGlueAction extends JosmAction { … … 76 73 public void actionPerformed(ActionEvent e) { 77 74 try { 78 unglue( e);75 unglue(); 79 76 } catch (UserCancelException ignore) { 80 77 Logging.trace(ignore); … … 84 81 } 85 82 86 protected void unglue( ActionEvent e) throws UserCancelException {83 protected void unglue() throws UserCancelException { 87 84 88 85 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected(); … … 90 87 String errMsg = null; 91 88 int errorTime = Notification.TIME_DEFAULT; 89 92 90 if (checkSelectionOneNodeAtMostOneWay(selection)) { 93 91 checkAndConfirmOutlyingUnglue(); 94 int count = 0; 95 for (Way w : selectedNode.getParentWays()) { 96 if (!w.isUsable() || w.getNodesCount() < 1) { 97 continue; 98 } 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."); 92 List<Way> parentWays = selectedNode.getParentWays().stream().filter(Way::isUsable).collect(Collectors.toList()); 93 94 if (parentWays.size() < 2) { 95 if (!parentWays.isEmpty()) { 96 // single way 97 Way way = selectedWay == null ? parentWays.get(0) : selectedWay; 98 boolean closedOrSelfCrossing = way.getNodes().stream().filter(n -> n == selectedNode).count() > 1; 99 100 final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary( 101 Collections.singleton(selectedNode), !selectedNode.isTagged()); 102 if (dialog != null) { 103 unglueOneNodeAtMostOneWay(way, dialog); 104 return; 105 } else if (closedOrSelfCrossing) { 106 unglueClosedOrSelfCrossingWay(way, dialog); 107 return; 115 108 } 109 } 110 errorTime = Notification.TIME_SHORT; 111 errMsg = tr("This node is not glued to anything else."); 116 112 } else { 117 113 // and then do the work. … … 119 115 } 120 116 } else if (checkSelectionOneWayAnyNodes(selection)) { 117 selectedNodes.removeIf(n -> n.getParentWays().stream().filter(Way::isUsable).count() < 2); 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 if (selectedNodes.isEmpty()) { 136 120 if (selection.size() > 1) { 137 121 errMsg = tr("None of these nodes are glued to anything else."); … … 139 123 errMsg = tr("None of this way''s nodes are glued to anything else."); 140 124 } 125 } else if (selectedNodes.size() == 1) { 126 selectedNode = selectedNodes.iterator().next(); 127 unglueWays(); 141 128 } else { 142 129 // and then do the work. 143 selectedNodes = tmpNodes;144 130 unglueOneWayAnyNodes(); 145 131 } … … 176 162 } 177 163 178 static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, Collection<Command> cmds) {164 static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, List<Command> cmds) { 179 165 updateMemberships(dialog.getMemberships().orElse(null), existingNode, newNodes, cmds); 180 166 updateProperties(dialog.getTags().orElse(null), existingNode, newNodes, cmds); 181 167 } 182 168 183 private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, Collection<Command> cmds) {169 private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, List<Command> cmds) { 184 170 if (ExistingBothNew.NEW == tags) { 185 171 final Node newSelectedNode = new Node(existingNode); … … 195 181 /** 196 182 * 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 199 */ 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; 207 } 208 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 183 * (i.e. copy node and remove all tags from the old one.) 184 * @param way way to modify 185 * @param dialog the user dialog 186 */ 187 private void unglueOneNodeAtMostOneWay(Way way, PropertiesMembershipChoiceDialog dialog) { 188 List<Command> cmds = new ArrayList<>(); 189 List<Node> newNodes = new ArrayList<>(); 190 Way modWay = modifyWay(selectedNode, way, cmds, newNodes); 191 cmds.add(new ChangeNodesCommand(way, modWay.getNodes())); 192 if (dialog != null) { 193 update(dialog, selectedNode, newNodes, cmds); 194 } 195 196 // Place the selected node where the cursor is or some pixels above 230 197 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 } 238 } 239 198 Point currMousePos = mv.getMousePosition(); 199 if (currMousePos != null) { 200 cmds.add(new MoveCommand(selectedNode, mv.getLatLon(currMousePos.getX(), currMousePos.getY()))); 201 } else { 202 cmds.add(new MoveCommand(selectedNode, 0, 5)); 203 } 240 204 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Unglued Node"), cmds)); 241 getLayerManager().getEditDataSet().setSelected(moveSelectedNode ? selectedNode : unglued); 242 mv.repaint(); 243 } 244 245 /** 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 does 248 * not make sense. Due to the call order in actionPerformed, this is only called when the 249 * node is only part of one or less ways. 250 * 251 * @param selection The selection to check against 252 * @return {@code true} if selection is suitable 253 */ 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(); 205 getLayerManager().getEditDataSet().setSelected(selectedNode); 265 206 } 266 207 … … 269 210 * Checks only if the number and type of items selected looks good. 270 211 * 271 * If this method returns "true", selectedNode and selectedWay will be set.212 * If this method returns "true", selectedNode will be set, selectedWay might be set 272 213 * 273 214 * Returns true if either one node is selected or one node and one … … 362 303 private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { 363 304 // clone the node for the way 364 Node newNode = new Node(originalNode, true /* clear OSM ID */);305 Node newNode = cloneNode(originalNode, cmds); 365 306 newNodes.add(newNode); 366 cmds.add(new AddCommand(originalNode.getDataSet(), newNode)); 367 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 } 307 308 List<Node> nn = new ArrayList<>(w.getNodes()); 309 nn.replaceAll(n -> n == originalNode ? newNode : n); 375 310 Way newWay = new Way(w); 376 311 newWay.setNodes(nn); 377 312 378 313 return newWay; 314 } 315 316 private static Node cloneNode(Node originalNode, List<Command> cmds) { 317 Node newNode = new Node(originalNode, true /* clear OSM ID */); 318 cmds.add(new AddCommand(originalNode.getDataSet(), newNode)); 319 return newNode; 379 320 } 380 321 … … 386 327 * @param newNodes List of nodes that contain the new node 387 328 */ 388 private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, Collection<Command> cmds) {329 private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, List<Command> cmds) { 389 330 if (memberships == null || ExistingBothNew.OLD == memberships) { 390 331 return; … … 422 363 * 423 364 * dupe a single node once, and put the copy on the selected way 424 */ 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<>(); 365 * @throws UserCancelException if user cancels choice 366 */ 367 private void unglueWays() throws UserCancelException { 368 final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog 369 .showIfNecessary(Collections.singleton(selectedNode), false); 370 List<Command> cmds = new ArrayList<>(); 371 List<Node> newNodes = new ArrayList<>(); 372 List<Way> parentWays; 436 373 if (selectedWay == null) { 437 LinkedList<Way> parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable) 438 .collect(Collectors.toCollection(LinkedList::new)); 374 parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable).collect(Collectors.toList()); 439 375 // see #5452 and #18670 440 376 parentWays.sort((o1, o2) -> { … … 449 385 }); 450 386 // 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); 387 parentWays.remove(0); 460 388 } else { 461 Way modWay = modifyWay(selectedNode, selectedWay, cmds, newNodes); 462 addCheckedChangeNodesCmd(cmds, selectedWay, modWay.getNodes()); 389 parentWays = Collections.singletonList(selectedWay); 390 } 391 Set<Way> warnParents = new HashSet<>(); 392 for (Way w : parentWays) { 393 if (w.isFirstLastNode(selectedNode)) 394 warnParents.add(w); 395 cmds.add(new ChangeNodesCommand(w, modifyWay(selectedNode, w, cmds, newNodes).getNodes())); 463 396 } 464 397 … … 466 399 update(dialog, selectedNode, newNodes, cmds); 467 400 } 401 notifyWayPartOfRelation(warnParents); 468 402 469 403 execCommands(cmds, newNodes); … … 484 418 /** 485 419 * Duplicates a node used several times by the same way. See #9896. 420 * First occurrence is kept. A closed way will be "opened" when the closing node is unglued. 421 * @param way way to modify 422 * @param dialog user dialog, might be null 486 423 * @return true if action is OK false if there is nothing to do 487 424 */ 488 private boolean unglue SelfCrossingWay() {425 private boolean unglueClosedOrSelfCrossingWay(Way way, PropertiesMembershipChoiceDialog dialog) { 489 426 // 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<>(); 427 List<Command> cmds = new ArrayList<>(); 500 428 List<Node> oldNodes = way.getNodes(); 501 429 List<Node> newNodes = new ArrayList<>(oldNodes.size()); 502 430 List<Node> addNodes = new ArrayList<>(); 503 boolean seen = false;431 int count = 0; 504 432 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); 517 } 433 if (n == selectedNode && count++ > 0) { 434 n = cloneNode(selectedNode, cmds); 435 addNodes.add(n); 436 } 437 newNodes.add(n); 518 438 } 519 439 if (addNodes.isEmpty()) { … … 521 441 return false; 522 442 } 443 if (dialog != null) { 444 update(dialog, selectedNode, addNodes, cmds); 445 } 523 446 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; 447 execCommands(cmds, addNodes); 448 return true; 536 449 } 537 450 538 451 /** 539 452 * dupe all nodes that are selected, and put the copies on the selected way 540 * 541 */ 542 private void unglueOneWayAnyNodes() { 543 Way tmpWay = selectedWay; 544 545 final PropertiesMembershipChoiceDialog dialog; 546 try { 547 dialog = PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false); 548 } catch (UserCancelException e) { 549 Logging.trace(e); 550 return; 551 } 552 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); 562 } 453 * @throws UserCancelException 454 * 455 */ 456 private void unglueOneWayAnyNodes() throws UserCancelException { 457 final PropertiesMembershipChoiceDialog dialog = 458 PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false); 459 460 Map<Node, Node> replaced = new HashMap<>(); 461 List<Command> cmds = new ArrayList<>(); 462 463 selectedNodes.forEach(n -> replaced.put(n, cloneNode(n, cmds))); 464 List<Node> modNodes = new ArrayList<>(selectedWay.getNodes()); 465 modNodes.replaceAll(n -> replaced.getOrDefault(n, n)); 466 467 if (dialog != null) { 468 replaced.forEach((k, v) -> update(dialog, k, Collections.singletonList(v), cmds)); 469 } 470 563 471 // only one changeCommand for a way, else garbage will happen 564 addCheckedChangeNodesCmd(cmds, selectedWay, tmpWay.getNodes()); 565 472 addCheckedChangeNodesCmd(cmds, selectedWay, modNodes); 566 473 UndoRedoHandler.getInstance().add(new SequenceCommand( 567 474 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);570 } 571 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);475 selectedNodes.size(), selectedNodes.size(), 2 * selectedNodes.size()), cmds)); 476 getLayerManager().getEditDataSet().setSelected(replaced.values()); 477 } 478 479 private boolean addCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) { 480 boolean relationCheck = !calcAffectedRelations(Collections.singleton(w)).isEmpty(); 574 481 cmds.add(new ChangeNodesCommand(w, nodes)); 575 482 if (relationCheck) { 576 483 notifyWayPartOfRelation(Collections.singleton(w)); 577 484 } 485 return relationCheck; 578 486 } 579 487 … … 612 520 613 521 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 } 522 Set<Relation> affectedRelations = calcAffectedRelations(ways); 637 523 if (affectedRelations.isEmpty()) { 638 524 return; 639 525 } 640 641 526 final int size = affectedRelations.size(); 642 527 final String msg1 = trn("Unglueing possibly affected {0} relation: {1}", "Unglueing possibly affected {0} relations: {1}", 643 size, size, Utils.joinAsHtmlUnorderedList(affectedRelations));528 size, size, DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(affectedRelations, 20)); 644 529 final String msg2 = trn("Ensure that the relation has not been broken!", "Ensure that the relations have not been broken!", 645 530 size); 646 new Notification("<html>" + msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show(); 531 new Notification(msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show(); 532 } 533 534 protected Set<Relation> calcAffectedRelations(final Collection<Way> ways) { 535 final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode); 536 return OsmPrimitive.getParentRelations(ways) 537 .stream().filter(r -> isRelationAffected(r, affectedNodes, ways)) 538 .collect(Collectors.toSet()); 539 } 540 541 private static boolean isRelationAffected(Relation r, Set<Node> affectedNodes, Collection<Way> ways) { 542 if (!r.isUsable()) 543 return false; 544 // see #18670: suppress notification when well known restriction types are not affected 545 if (!r.hasTag("type", "restriction", "connectivity", "destination_sign") || r.hasIncompleteMembers()) 546 return true; 547 int count = 0; 548 for (RelationMember rm : r.getMembers()) { 549 if (rm.isNode() && affectedNodes.contains(rm.getNode())) 550 count++; 551 if (rm.isWay() && ways.contains(rm.getWay())) { 552 count++; 553 if ("via".equals(rm.getRole())) { 554 count++; 555 } 556 } 557 } 558 return count >= 2; 647 559 } 648 560 } -
trunk/src/org/openstreetmap/josm/gui/dialogs/PropertiesMembershipChoiceDialog.java
r14662 r16362 38 38 public enum ExistingBothNew { 39 39 OLD, BOTH, NEW; 40 41 /**42 * Returns the opposite/inverted user choice.43 * @return the opposite/inverted user choice44 */45 public ExistingBothNew opposite() {46 return equals(OLD) ? NEW : equals(NEW) ? OLD : this;47 }48 40 } 49 41
Note:
See TracChangeset
for help on using the changeset viewer.