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