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         * Returns the opposite/inverted user choice.
043         * @return the opposite/inverted user choice
044         */
045        public ExistingBothNew opposite() {
046            return equals(OLD) ? NEW : equals(NEW) ? OLD : this;
047        }
048    }
049
050    /**
051     * Provides toggle buttons to allow the user choose the existing node, the new nodes, or all of them.
052     */
053    private static class ExistingBothNewChoice {
054        /** The "Existing node" button */
055        final AbstractButton oldNode = new JToggleButton(tr("Existing node"), ImageProvider.get("dialogs/conflict/tagkeeptheir"));
056        /** The "Both nodes" button */
057        final AbstractButton bothNodes = new JToggleButton(tr("Both nodes"), ImageProvider.get("dialogs/conflict/tagundecide"));
058        /** The "New node" button */
059        final AbstractButton newNode = new JToggleButton(tr("New node"), ImageProvider.get("dialogs/conflict/tagkeepmine"));
060
061        ExistingBothNewChoice(final boolean preselectNew) {
062            final ButtonGroup tagsGroup = new ButtonGroup();
063            tagsGroup.add(oldNode);
064            tagsGroup.add(bothNodes);
065            tagsGroup.add(newNode);
066            tagsGroup.setSelected((preselectNew ? newNode : oldNode).getModel(), true);
067        }
068
069        void add(JPanel content, int gridy) {
070            content.add(oldNode, GBC.std(1, gridy));
071            content.add(bothNodes, GBC.std(2, gridy));
072            content.add(newNode, GBC.std(3, gridy));
073        }
074
075        ExistingBothNew getSelected() {
076            if (oldNode.isSelected()) {
077                return ExistingBothNew.OLD;
078            } else if (bothNodes.isSelected()) {
079                return ExistingBothNew.BOTH;
080            } else if (newNode.isSelected()) {
081                return ExistingBothNew.NEW;
082            } else {
083                throw new IllegalStateException();
084            }
085        }
086    }
087
088    private PropertiesMembershipChoiceDialog(boolean preselectNew, boolean queryTags, boolean queryMemberships) {
089        super(MainApplication.getMainFrame(), tr("Tags/Memberships"), tr("Unglue"), tr("Cancel"));
090        setButtonIcons("unglueways", "cancel");
091
092        final JPanel content = new JPanel(new GridBagLayout());
093
094        if (queryTags) {
095            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));
096            tags = new ExistingBothNewChoice(preselectNew);
097            tags.add(content, 2);
098        } else {
099            tags = null;
100        }
101
102        if (queryMemberships) {
103            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));
104            memberships = new ExistingBothNewChoice(preselectNew);
105            memberships.add(content, 4);
106        } else {
107            memberships = null;
108        }
109
110        setContent(content);
111        setResizable(false);
112    }
113
114    /**
115     * Returns the tags choice.
116     * @return the tags choice
117     */
118    public Optional<ExistingBothNew> getTags() {
119        return Optional.ofNullable(tags).map(ExistingBothNewChoice::getSelected);
120    }
121
122    /**
123     * Returns the memberships choice.
124     * @return the memberships choice
125     */
126    public Optional<ExistingBothNew> getMemberships() {
127        return Optional.ofNullable(memberships).map(ExistingBothNewChoice::getSelected);
128    }
129
130    /**
131     * Creates and shows a new {@code PropertiesMembershipChoiceDialog} if necessary. Otherwise does nothing.
132     * @param selectedNodes selected nodes
133     * @param preselectNew if {@code true}, pre-select "new node" as default choice
134     * @return A new {@code PropertiesMembershipChoiceDialog} that has been shown to user, or {@code null}
135     * @throws UserCancelException if user cancels choice
136     */
137    public static PropertiesMembershipChoiceDialog showIfNecessary(Collection<Node> selectedNodes, boolean preselectNew)
138            throws UserCancelException {
139        final boolean queryTags = isTagged(selectedNodes);
140        final boolean queryMemberships = isUsedInRelations(selectedNodes);
141        if (queryTags || queryMemberships) {
142            final PropertiesMembershipChoiceDialog dialog = new PropertiesMembershipChoiceDialog(preselectNew, queryTags, queryMemberships);
143            dialog.showDialog();
144            if (dialog.getValue() != 1) {
145                throw new UserCancelException();
146            }
147            return dialog;
148        }
149        return null;
150    }
151
152    private static boolean isTagged(final Collection<Node> existingNodes) {
153        return existingNodes.stream().anyMatch(Node::hasKeys);
154    }
155
156    private static boolean isUsedInRelations(final Collection<Node> existingNodes) {
157        return existingNodes.stream().anyMatch(
158                selectedNode -> selectedNode.referrers(Relation.class).anyMatch(Objects::nonNull));
159    }
160}