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