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.GridBagLayout; 009import java.awt.event.ActionEvent; 010import java.awt.event.KeyEvent; 011import java.util.ArrayList; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.HashMap; 015import java.util.HashSet; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.Map; 019import java.util.Set; 020 021import javax.swing.AbstractButton; 022import javax.swing.ButtonGroup; 023import javax.swing.JLabel; 024import javax.swing.JOptionPane; 025import javax.swing.JPanel; 026import javax.swing.JToggleButton; 027 028import org.openstreetmap.josm.Main; 029import org.openstreetmap.josm.command.AddCommand; 030import org.openstreetmap.josm.command.ChangeCommand; 031import org.openstreetmap.josm.command.ChangeNodesCommand; 032import org.openstreetmap.josm.command.Command; 033import org.openstreetmap.josm.command.SequenceCommand; 034import org.openstreetmap.josm.data.osm.Node; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.Relation; 037import org.openstreetmap.josm.data.osm.RelationMember; 038import org.openstreetmap.josm.data.osm.Way; 039import org.openstreetmap.josm.gui.DefaultNameFormatter; 040import org.openstreetmap.josm.gui.ExtendedDialog; 041import org.openstreetmap.josm.gui.MapView; 042import org.openstreetmap.josm.gui.Notification; 043import org.openstreetmap.josm.tools.GBC; 044import org.openstreetmap.josm.tools.ImageProvider; 045import org.openstreetmap.josm.tools.Predicate; 046import org.openstreetmap.josm.tools.Shortcut; 047import org.openstreetmap.josm.tools.UserCancelException; 048import org.openstreetmap.josm.tools.Utils; 049 050/** 051 * Duplicate nodes that are used by multiple ways. 052 * 053 * Resulting nodes are identical, up to their position. 054 * 055 * This is the opposite of the MergeNodesAction. 056 * 057 * If a single node is selected, it will copy that node and remove all tags from the old one 058 */ 059public class UnGlueAction extends JosmAction { 060 061 private transient Node selectedNode; 062 private transient Way selectedWay; 063 private transient Set<Node> selectedNodes; 064 065 /** 066 * Create a new UnGlueAction. 067 */ 068 public UnGlueAction() { 069 super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."), 070 Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.DIRECT), true); 071 putValue("help", ht("/Action/UnGlue")); 072 } 073 074 /** 075 * Called when the action is executed. 076 * 077 * This method does some checking on the selection and calls the matching unGlueWay method. 078 */ 079 @Override 080 public void actionPerformed(ActionEvent e) { 081 try { 082 unglue(e); 083 } catch (UserCancelException ignore) { 084 Main.trace(ignore); 085 } finally { 086 cleanup(); 087 } 088 } 089 090 protected void unglue(ActionEvent e) throws UserCancelException { 091 092 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected(); 093 094 String errMsg = null; 095 int errorTime = Notification.TIME_DEFAULT; 096 if (checkSelectionOneNodeAtMostOneWay(selection)) { 097 checkAndConfirmOutlyingUnglue(); 098 int count = 0; 099 for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { 100 if (!w.isUsable() || w.getNodesCount() < 1) { 101 continue; 102 } 103 count++; 104 } 105 if (count < 2) { 106 boolean selfCrossing = false; 107 if (count == 1) { 108 // First try unglue self-crossing way 109 selfCrossing = unglueSelfCrossingWay(); 110 } 111 // If there aren't enough ways, maybe the user wanted to unglue the nodes 112 // (= copy tags to a new node) 113 if (!selfCrossing) 114 if (checkForUnglueNode(selection)) { 115 unglueOneNodeAtMostOneWay(e); 116 } else { 117 errorTime = Notification.TIME_SHORT; 118 errMsg = tr("This node is not glued to anything else."); 119 } 120 } else { 121 // and then do the work. 122 unglueWays(); 123 } 124 } else if (checkSelectionOneWayAnyNodes(selection)) { 125 checkAndConfirmOutlyingUnglue(); 126 Set<Node> tmpNodes = new HashSet<>(); 127 for (Node n : selectedNodes) { 128 int count = 0; 129 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) { 130 if (!w.isUsable()) { 131 continue; 132 } 133 count++; 134 } 135 if (count >= 2) { 136 tmpNodes.add(n); 137 } 138 } 139 if (tmpNodes.isEmpty()) { 140 if (selection.size() > 1) { 141 errMsg = tr("None of these nodes are glued to anything else."); 142 } else { 143 errMsg = tr("None of this way''s nodes are glued to anything else."); 144 } 145 } else { 146 // and then do the work. 147 selectedNodes = tmpNodes; 148 unglueOneWayAnyNodes(); 149 } 150 } else { 151 errorTime = Notification.TIME_VERY_LONG; 152 errMsg = 153 tr("The current selection cannot be used for unglueing.")+'\n'+ 154 '\n'+ 155 tr("Select either:")+'\n'+ 156 tr("* One tagged node, or")+'\n'+ 157 tr("* One node that is used by more than one way, or")+'\n'+ 158 tr("* One node that is used by more than one way and one of those ways, or")+'\n'+ 159 tr("* One way that has one or more nodes that are used by more than one way, or")+'\n'+ 160 tr("* One way and one or more of its nodes that are used by more than one way.")+'\n'+ 161 '\n'+ 162 tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+ 163 "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+ 164 "own copy and all nodes will be selected."); 165 } 166 167 if (errMsg != null) { 168 new Notification( 169 errMsg) 170 .setIcon(JOptionPane.ERROR_MESSAGE) 171 .setDuration(errorTime) 172 .show(); 173 } 174 } 175 176 private void cleanup() { 177 selectedNode = null; 178 selectedWay = null; 179 selectedNodes = null; 180 } 181 182 /** 183 * Provides toggle buttons to allow the user choose the existing node, the new nodes, or all of them. 184 */ 185 private static class ExistingBothNewChoice { 186 final AbstractButton oldNode = new JToggleButton(tr("Existing node"), ImageProvider.get("dialogs/conflict/tagkeeptheir")); 187 final AbstractButton bothNodes = new JToggleButton(tr("Both nodes"), ImageProvider.get("dialogs/conflict/tagundecide")); 188 final AbstractButton newNode = new JToggleButton(tr("New node"), ImageProvider.get("dialogs/conflict/tagkeepmine")); 189 190 ExistingBothNewChoice(final boolean preselectNew) { 191 final ButtonGroup tagsGroup = new ButtonGroup(); 192 tagsGroup.add(oldNode); 193 tagsGroup.add(bothNodes); 194 tagsGroup.add(newNode); 195 tagsGroup.setSelected((preselectNew ? newNode : oldNode).getModel(), true); 196 } 197 } 198 199 /** 200 * A dialog allowing the user decide whether the tags/memberships of the existing node should afterwards be at 201 * the existing node, the new nodes, or all of them. 202 */ 203 static final class PropertiesMembershipDialog extends ExtendedDialog { 204 205 final transient ExistingBothNewChoice tags; 206 final transient ExistingBothNewChoice memberships; 207 208 private PropertiesMembershipDialog(boolean preselectNew, boolean queryTags, boolean queryMemberships) { 209 super(Main.parent, tr("Tags / Memberships"), new String[]{tr("Unglue"), tr("Cancel")}); 210 setButtonIcons(new String[]{"unglueways", "cancel"}); 211 212 final JPanel content = new JPanel(new GridBagLayout()); 213 214 if (queryTags) { 215 content.add(new JLabel(tr("Where should the tags of the node be put?")), GBC.std(1, 1).span(3).insets(0, 20, 0, 0)); 216 tags = new ExistingBothNewChoice(preselectNew); 217 content.add(tags.oldNode, GBC.std(1, 2)); 218 content.add(tags.bothNodes, GBC.std(2, 2)); 219 content.add(tags.newNode, GBC.std(3, 2)); 220 } else { 221 tags = null; 222 } 223 224 if (queryMemberships) { 225 content.add(new JLabel(tr("Where should the memberships of this node be put?")), GBC.std(1, 3).span(3).insets(0, 20, 0, 0)); 226 memberships = new ExistingBothNewChoice(preselectNew); 227 content.add(memberships.oldNode, GBC.std(1, 4)); 228 content.add(memberships.bothNodes, GBC.std(2, 4)); 229 content.add(memberships.newNode, GBC.std(3, 4)); 230 } else { 231 memberships = null; 232 } 233 234 setContent(content); 235 setResizable(false); 236 } 237 238 static PropertiesMembershipDialog showIfNecessary(Iterable<Node> selectedNodes, boolean preselectNew) throws UserCancelException { 239 final boolean tagged = isTagged(selectedNodes); 240 final boolean usedInRelations = isUsedInRelations(selectedNodes); 241 if (tagged || usedInRelations) { 242 final PropertiesMembershipDialog dialog = new PropertiesMembershipDialog(preselectNew, tagged, usedInRelations); 243 dialog.showDialog(); 244 if (dialog.getValue() != 1) { 245 throw new UserCancelException(); 246 } 247 return dialog; 248 } 249 return null; 250 } 251 252 private static boolean isTagged(final Iterable<Node> existingNodes) { 253 return Utils.exists(existingNodes, new Predicate<Node>() { 254 @Override 255 public boolean evaluate(final Node selectedNode) { 256 return selectedNode.hasKeys(); 257 } 258 }); 259 } 260 261 private static boolean isUsedInRelations(final Iterable<Node> existingNodes) { 262 return Utils.exists(existingNodes, new Predicate<Node>() { 263 @Override 264 public boolean evaluate(final Node selectedNode) { 265 return Utils.exists(selectedNode.getReferrers(), OsmPrimitive.relationPredicate); 266 } 267 }); 268 } 269 270 void update(final Node existingNode, final List<Node> newNodes, final Collection<Command> cmds) { 271 updateMemberships(existingNode, newNodes, cmds); 272 updateProperties(existingNode, newNodes, cmds); 273 } 274 275 private void updateProperties(final Node existingNode, final Iterable<Node> newNodes, final Collection<Command> cmds) { 276 if (tags != null && tags.newNode.isSelected()) { 277 final Node newSelectedNode = new Node(existingNode); 278 newSelectedNode.removeAll(); 279 cmds.add(new ChangeCommand(existingNode, newSelectedNode)); 280 } else if (tags != null && tags.oldNode.isSelected()) { 281 for (Node newNode : newNodes) { 282 newNode.removeAll(); 283 } 284 } 285 } 286 287 private void updateMemberships(final Node existingNode, final List<Node> newNodes, final Collection<Command> cmds) { 288 if (memberships != null && memberships.bothNodes.isSelected()) { 289 fixRelations(existingNode, cmds, newNodes, false); 290 } else if (memberships != null && memberships.newNode.isSelected()) { 291 fixRelations(existingNode, cmds, newNodes, true); 292 } 293 } 294 } 295 296 /** 297 * Assumes there is one tagged Node stored in selectedNode that it will try to unglue. 298 * (i.e. copy node and remove all tags from the old one. Relations will not be removed) 299 * @param e event that trigerred the action 300 */ 301 private void unglueOneNodeAtMostOneWay(ActionEvent e) { 302 final PropertiesMembershipDialog dialog; 303 try { 304 dialog = PropertiesMembershipDialog.showIfNecessary(Collections.singleton(selectedNode), true); 305 } catch (UserCancelException ex) { 306 Main.trace(ex); 307 return; 308 } 309 310 final Node n = new Node(selectedNode, true); 311 312 List<Command> cmds = new LinkedList<>(); 313 cmds.add(new AddCommand(n)); 314 if (dialog != null) { 315 dialog.update(selectedNode, Collections.singletonList(n), cmds); 316 } 317 318 // If this wasn't called from menu, place it where the cursor is/was 319 if (e.getSource() instanceof JPanel) { 320 MapView mv = Main.map.mapView; 321 n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY())); 322 } 323 324 Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds)); 325 getLayerManager().getEditDataSet().setSelected(n); 326 Main.map.mapView.repaint(); 327 } 328 329 /** 330 * Checks if selection is suitable for ungluing. This is the case when there's a single, 331 * tagged node selected that's part of at least one way (ungluing an unconnected node does 332 * not make sense. Due to the call order in actionPerformed, this is only called when the 333 * node is only part of one or less ways. 334 * 335 * @param selection The selection to check against 336 * @return {@code true} if selection is suitable 337 */ 338 private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) { 339 if (selection.size() != 1) 340 return false; 341 OsmPrimitive n = (OsmPrimitive) selection.toArray()[0]; 342 if (!(n instanceof Node)) 343 return false; 344 if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty()) 345 return false; 346 347 selectedNode = (Node) n; 348 return selectedNode.isTagged(); 349 } 350 351 /** 352 * Checks if the selection consists of something we can work with. 353 * Checks only if the number and type of items selected looks good. 354 * 355 * If this method returns "true", selectedNode and selectedWay will be set. 356 * 357 * Returns true if either one node is selected or one node and one 358 * way are selected and the node is part of the way. 359 * 360 * The way will be put into the object variable "selectedWay", the node into "selectedNode". 361 * @param selection selected primitives 362 * @return true if either one node is selected or one node and one way are selected and the node is part of the way 363 */ 364 private boolean checkSelectionOneNodeAtMostOneWay(Collection<? extends OsmPrimitive> selection) { 365 366 int size = selection.size(); 367 if (size < 1 || size > 2) 368 return false; 369 370 selectedNode = null; 371 selectedWay = null; 372 373 for (OsmPrimitive p : selection) { 374 if (p instanceof Node) { 375 selectedNode = (Node) p; 376 if (size == 1 || selectedWay != null) 377 return size == 1 || selectedWay.containsNode(selectedNode); 378 } else if (p instanceof Way) { 379 selectedWay = (Way) p; 380 if (size == 2 && selectedNode != null) 381 return selectedWay.containsNode(selectedNode); 382 } 383 } 384 385 return false; 386 } 387 388 /** 389 * Checks if the selection consists of something we can work with. 390 * Checks only if the number and type of items selected looks good. 391 * 392 * Returns true if one way and any number of nodes that are part of that way are selected. 393 * Note: "any" can be none, then all nodes of the way are used. 394 * 395 * The way will be put into the object variable "selectedWay", the nodes into "selectedNodes". 396 * @param selection selected primitives 397 * @return true if one way and any number of nodes that are part of that way are selected 398 */ 399 private boolean checkSelectionOneWayAnyNodes(Collection<? extends OsmPrimitive> selection) { 400 if (selection.isEmpty()) 401 return false; 402 403 selectedWay = null; 404 for (OsmPrimitive p : selection) { 405 if (p instanceof Way) { 406 if (selectedWay != null) 407 return false; 408 selectedWay = (Way) p; 409 } 410 } 411 if (selectedWay == null) 412 return false; 413 414 selectedNodes = new HashSet<>(); 415 for (OsmPrimitive p : selection) { 416 if (p instanceof Node) { 417 Node n = (Node) p; 418 if (!selectedWay.containsNode(n)) 419 return false; 420 selectedNodes.add(n); 421 } 422 } 423 424 if (selectedNodes.isEmpty()) { 425 selectedNodes.addAll(selectedWay.getNodes()); 426 } 427 428 return true; 429 } 430 431 /** 432 * dupe the given node of the given way 433 * 434 * assume that originalNode is in the way 435 * <ul> 436 * <li>the new node will be put into the parameter newNodes.</li> 437 * <li>the add-node command will be put into the parameter cmds.</li> 438 * <li>the changed way will be returned and must be put into cmds by the caller!</li> 439 * </ul> 440 * @param originalNode original node to duplicate 441 * @param w parent way 442 * @param cmds List of commands that will contain the new "add node" command 443 * @param newNodes List of nodes that will contain the new node 444 * @return new way The modified way. Change command mus be handled by the caller 445 */ 446 private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) { 447 // clone the node for the way 448 Node newNode = new Node(originalNode, true /* clear OSM ID */); 449 newNodes.add(newNode); 450 cmds.add(new AddCommand(newNode)); 451 452 List<Node> nn = new ArrayList<>(); 453 for (Node pushNode : w.getNodes()) { 454 if (originalNode == pushNode) { 455 pushNode = newNode; 456 } 457 nn.add(pushNode); 458 } 459 Way newWay = new Way(w); 460 newWay.setNodes(nn); 461 462 return newWay; 463 } 464 465 /** 466 * put all newNodes into the same relation(s) that originalNode is in 467 * @param originalNode original node to duplicate 468 * @param cmds List of commands that will contain the new "change relation" commands 469 * @param newNodes List of nodes that contain the new node 470 * @param removeOldMember whether the membership of the "old node" should be removed 471 */ 472 private static void fixRelations(Node originalNode, Collection<Command> cmds, List<Node> newNodes, boolean removeOldMember) { 473 // modify all relations containing the node 474 for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) { 475 if (r.isDeleted()) { 476 continue; 477 } 478 Relation newRel = null; 479 Map<String, Integer> rolesToReAdd = null; // <role name, index> 480 int i = 0; 481 for (RelationMember rm : r.getMembers()) { 482 if (rm.isNode() && rm.getMember() == originalNode) { 483 if (newRel == null) { 484 newRel = new Relation(r); 485 rolesToReAdd = new HashMap<>(); 486 } 487 if (rolesToReAdd != null) { 488 rolesToReAdd.put(rm.getRole(), i); 489 } 490 } 491 i++; 492 } 493 if (newRel != null) { 494 if (rolesToReAdd != null) { 495 for (Map.Entry<String, Integer> role : rolesToReAdd.entrySet()) { 496 for (Node n : newNodes) { 497 newRel.addMember(role.getValue() + 1, new RelationMember(role.getKey(), n)); 498 } 499 if (removeOldMember) { 500 newRel.removeMember(role.getValue()); 501 } 502 } 503 } 504 cmds.add(new ChangeCommand(r, newRel)); 505 } 506 } 507 } 508 509 /** 510 * dupe a single node into as many nodes as there are ways using it, OR 511 * 512 * dupe a single node once, and put the copy on the selected way 513 */ 514 private void unglueWays() { 515 final PropertiesMembershipDialog dialog; 516 try { 517 dialog = PropertiesMembershipDialog.showIfNecessary(Collections.singleton(selectedNode), false); 518 } catch (UserCancelException e) { 519 Main.trace(e); 520 return; 521 } 522 523 List<Command> cmds = new LinkedList<>(); 524 List<Node> newNodes = new LinkedList<>(); 525 if (selectedWay == null) { 526 Way wayWithSelectedNode = null; 527 LinkedList<Way> parentWays = new LinkedList<>(); 528 for (OsmPrimitive osm : selectedNode.getReferrers()) { 529 if (osm.isUsable() && osm instanceof Way) { 530 Way w = (Way) osm; 531 if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) { 532 wayWithSelectedNode = w; 533 } else { 534 parentWays.add(w); 535 } 536 } 537 } 538 if (wayWithSelectedNode == null) { 539 parentWays.removeFirst(); 540 } 541 for (Way w : parentWays) { 542 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes))); 543 } 544 notifyWayPartOfRelation(parentWays); 545 } else { 546 cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes))); 547 notifyWayPartOfRelation(Collections.singleton(selectedWay)); 548 } 549 550 if (dialog != null) { 551 dialog.update(selectedNode, newNodes, cmds); 552 } 553 554 execCommands(cmds, newNodes); 555 } 556 557 /** 558 * Add commands to undo-redo system. 559 * @param cmds Commands to execute 560 * @param newNodes New created nodes by this set of command 561 */ 562 private void execCommands(List<Command> cmds, List<Node> newNodes) { 563 Main.main.undoRedo.add(new SequenceCommand(/* for correct i18n of plural forms - see #9110 */ 564 trn("Dupe into {0} node", "Dupe into {0} nodes", newNodes.size() + 1L, newNodes.size() + 1L), cmds)); 565 // select one of the new nodes 566 getLayerManager().getEditDataSet().setSelected(newNodes.get(0)); 567 } 568 569 /** 570 * Duplicates a node used several times by the same way. See #9896. 571 * @return true if action is OK false if there is nothing to do 572 */ 573 private boolean unglueSelfCrossingWay() { 574 // According to previous check, only one valid way through that node 575 Way way = null; 576 for (Way w: OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) { 577 if (w.isUsable() && w.getNodesCount() >= 1) { 578 way = w; 579 } 580 } 581 if (way == null) { 582 return false; 583 } 584 List<Command> cmds = new LinkedList<>(); 585 List<Node> oldNodes = way.getNodes(); 586 List<Node> newNodes = new ArrayList<>(oldNodes.size()); 587 List<Node> addNodes = new ArrayList<>(); 588 boolean seen = false; 589 for (Node n: oldNodes) { 590 if (n == selectedNode) { 591 if (seen) { 592 Node newNode = new Node(n, true /* clear OSM ID */); 593 newNodes.add(newNode); 594 cmds.add(new AddCommand(newNode)); 595 newNodes.add(newNode); 596 addNodes.add(newNode); 597 } else { 598 newNodes.add(n); 599 seen = true; 600 } 601 } else { 602 newNodes.add(n); 603 } 604 } 605 if (addNodes.isEmpty()) { 606 // selectedNode doesn't need unglue 607 return false; 608 } 609 cmds.add(new ChangeNodesCommand(way, newNodes)); 610 notifyWayPartOfRelation(Collections.singleton(way)); 611 try { 612 final PropertiesMembershipDialog dialog = PropertiesMembershipDialog.showIfNecessary(Collections.singleton(selectedNode), false); 613 if (dialog != null) { 614 dialog.update(selectedNode, addNodes, cmds); 615 } 616 execCommands(cmds, addNodes); 617 return true; 618 } catch (UserCancelException ignore) { 619 Main.trace(ignore); 620 } 621 return false; 622 } 623 624 /** 625 * dupe all nodes that are selected, and put the copies on the selected way 626 * 627 */ 628 private void unglueOneWayAnyNodes() { 629 Way tmpWay = selectedWay; 630 631 final PropertiesMembershipDialog dialog; 632 try { 633 dialog = PropertiesMembershipDialog.showIfNecessary(selectedNodes, false); 634 } catch (UserCancelException e) { 635 Main.trace(e); 636 return; 637 } 638 639 List<Command> cmds = new LinkedList<>(); 640 List<Node> allNewNodes = new LinkedList<>(); 641 for (Node n : selectedNodes) { 642 List<Node> newNodes = new LinkedList<>(); 643 tmpWay = modifyWay(n, tmpWay, cmds, newNodes); 644 if (dialog != null) { 645 dialog.update(n, newNodes, cmds); 646 } 647 allNewNodes.addAll(newNodes); 648 } 649 cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen 650 notifyWayPartOfRelation(Collections.singleton(selectedWay)); 651 652 Main.main.undoRedo.add(new SequenceCommand( 653 trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes", 654 selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds)); 655 getLayerManager().getEditDataSet().setSelected(allNewNodes); 656 } 657 658 @Override 659 protected void updateEnabledState() { 660 updateEnabledStateOnCurrentSelection(); 661 } 662 663 @Override 664 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 665 setEnabled(selection != null && !selection.isEmpty()); 666 } 667 668 protected void checkAndConfirmOutlyingUnglue() throws UserCancelException { 669 List<OsmPrimitive> primitives = new ArrayList<>(2 + (selectedNodes == null ? 0 : selectedNodes.size())); 670 if (selectedNodes != null) 671 primitives.addAll(selectedNodes); 672 if (selectedNode != null) 673 primitives.add(selectedNode); 674 final boolean ok = Command.checkAndConfirmOutlyingOperation("unglue", 675 tr("Unglue confirmation"), 676 tr("You are about to unglue nodes outside of the area you have downloaded." 677 + "<br>" 678 + "This can cause problems because other objects (that you do not see) might use them." 679 + "<br>" 680 + "Do you really want to unglue?"), 681 tr("You are about to unglue incomplete objects." 682 + "<br>" 683 + "This will cause problems because you don''t see the real object." 684 + "<br>" + "Do you really want to unglue?"), 685 primitives, null); 686 if (!ok) { 687 throw new UserCancelException(); 688 } 689 } 690 691 protected void notifyWayPartOfRelation(final Iterable<Way> ways) { 692 final Set<String> affectedRelations = new HashSet<>(); 693 for (Way way : ways) { 694 for (OsmPrimitive ref : way.getReferrers()) { 695 if (ref instanceof Relation && ref.isUsable()) { 696 affectedRelations.add(ref.getDisplayName(DefaultNameFormatter.getInstance())); 697 } 698 } 699 } 700 if (affectedRelations.isEmpty()) { 701 return; 702 } 703 704 final String msg1 = trn("Unglueing affected {0} relation: {1}", "Unglueing affected {0} relations: {1}", 705 affectedRelations.size(), affectedRelations.size(), Utils.joinAsHtmlUnorderedList(affectedRelations)); 706 final String msg2 = trn("Ensure that the relation has not been broken!", "Ensure that the relations have not been broken!", 707 affectedRelations.size()); 708 new Notification("<html>" + msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show(); 709 } 710}