001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.util.Collection;
008import java.util.Objects;
009import java.util.Optional;
010
011import javax.swing.AbstractButton;
012import javax.swing.ButtonGroup;
013import javax.swing.JLabel;
014import javax.swing.JPanel;
015import javax.swing.JToggleButton;
016
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.gui.ExtendedDialog;
020import org.openstreetmap.josm.gui.MainApplication;
021import org.openstreetmap.josm.tools.GBC;
022import org.openstreetmap.josm.tools.ImageProvider;
023import org.openstreetmap.josm.tools.UserCancelException;
024
025/**
026 * A dialog allowing the user decide whether the tags/memberships of the existing node should afterwards be at
027 * the existing node, the new nodes, or all of them.
028 * @since 14320 (extracted from UnglueAction)
029 */
030public final class PropertiesMembershipChoiceDialog extends ExtendedDialog {
031
032    private final transient ExistingBothNewChoice tags;
033    private final transient ExistingBothNewChoice memberships;
034
035    /**
036     * Represents the user choice: the existing node, the new nodes, or all of them
037     */
038    public enum ExistingBothNew {
039        OLD, BOTH, NEW;
040    }
041
042    /**
043     * Provides toggle buttons to allow the user choose the existing node, the new nodes, or all of them.
044     */
045    private static class ExistingBothNewChoice {
046        /** The "Existing node" button */
047        final AbstractButton oldNode = new JToggleButton(tr("Existing node"), ImageProvider.get("dialogs/conflict/tagkeeptheir"));
048        /** The "Both nodes" button */
049        final AbstractButton bothNodes = new JToggleButton(tr("Both nodes"), ImageProvider.get("dialogs/conflict/tagundecide"));
050        /** The "New node" button */
051        final AbstractButton newNode = new JToggleButton(tr("New node"), ImageProvider.get("dialogs/conflict/tagkeepmine"));
052
053        ExistingBothNewChoice(final boolean preselectNew) {
054            final ButtonGroup tagsGroup = new ButtonGroup();
055            tagsGroup.add(oldNode);
056            tagsGroup.add(bothNodes);
057            tagsGroup.add(newNode);
058            tagsGroup.setSelected((preselectNew ? newNode : oldNode).getModel(), true);
059        }
060
061        void add(JPanel content, int gridy) {
062            content.add(oldNode, GBC.std(1, gridy));
063            content.add(bothNodes, GBC.std(2, gridy));
064            content.add(newNode, GBC.std(3, gridy));
065        }
066
067        ExistingBothNew getSelected() {
068            if (oldNode.isSelected()) {
069                return ExistingBothNew.OLD;
070            } else if (bothNodes.isSelected()) {
071                return ExistingBothNew.BOTH;
072            } else if (newNode.isSelected()) {
073                return ExistingBothNew.NEW;
074            } else {
075                throw new IllegalStateException();
076            }
077        }
078    }
079
080    private PropertiesMembershipChoiceDialog(boolean preselectNew, boolean queryTags, boolean queryMemberships) {
081        super(MainApplication.getMainFrame(), tr("Tags/Memberships"), tr("Unglue"), tr("Cancel"));
082        setButtonIcons("unglueways", "cancel");
083
084        final JPanel content = new JPanel(new GridBagLayout());
085
086        if (queryTags) {
087            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));
088            tags = new ExistingBothNewChoice(preselectNew);
089            tags.add(content, 2);
090        } else {
091            tags = null;
092        }
093
094        if (queryMemberships) {
095            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));
096            memberships = new ExistingBothNewChoice(preselectNew);
097            memberships.add(content, 4);
098        } else {
099            memberships = null;
100        }
101
102        setContent(content);
103        setResizable(false);
104    }
105
106    /**
107     * Returns the tags choice.
108     * @return the tags choice
109     */
110    public Optional<ExistingBothNew> getTags() {
111        return Optional.ofNullable(tags).map(ExistingBothNewChoice::getSelected);
112    }
113
114    /**
115     * Returns the memberships choice.
116     * @return the memberships choice
117     */
118    public Optional<ExistingBothNew> getMemberships() {
119        return Optional.ofNullable(memberships).map(ExistingBothNewChoice::getSelected);
120    }
121
122    /**
123     * Creates and shows a new {@code PropertiesMembershipChoiceDialog} if necessary. Otherwise does nothing.
124     * @param selectedNodes selected nodes
125     * @param preselectNew if {@code true}, pre-select "new node" as default choice
126     * @return A new {@code PropertiesMembershipChoiceDialog} that has been shown to user, or {@code null}
127     * @throws UserCancelException if user cancels choice
128     */
129    public static PropertiesMembershipChoiceDialog showIfNecessary(Collection<Node> selectedNodes, boolean preselectNew)
130            throws UserCancelException {
131        final boolean queryTags = isTagged(selectedNodes);
132        final boolean queryMemberships = isUsedInRelations(selectedNodes);
133        if (queryTags || queryMemberships) {
134            final PropertiesMembershipChoiceDialog dialog = new PropertiesMembershipChoiceDialog(preselectNew, queryTags, queryMemberships);
135            dialog.showDialog();
136            if (dialog.getValue() != 1) {
137                throw new UserCancelException();
138            }
139            return dialog;
140        }
141        return null;
142    }
143
144    private static boolean isTagged(final Collection<Node> existingNodes) {
145        return existingNodes.stream().anyMatch(Node::isTagged);
146    }
147
148    private static boolean isUsedInRelations(final Collection<Node> existingNodes) {
149        return existingNodes.stream().anyMatch(
150                selectedNode -> selectedNode.referrers(Relation.class).anyMatch(Objects::nonNull));
151    }
152}