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