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