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}