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}