001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.relation.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.util.ArrayList;
008import java.util.List;
009
010import javax.swing.JOptionPane;
011import javax.swing.SwingUtilities;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.command.AddCommand;
015import org.openstreetmap.josm.command.ChangeCommand;
016import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
017import org.openstreetmap.josm.data.conflict.Conflict;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.RelationMember;
020import org.openstreetmap.josm.gui.DefaultNameFormatter;
021import org.openstreetmap.josm.gui.HelpAwareOptionPane;
022import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
023import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
024import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
025import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel;
026import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
027import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
028import org.openstreetmap.josm.gui.layer.OsmDataLayer;
029import org.openstreetmap.josm.gui.tagging.TagEditorModel;
030import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
031import org.openstreetmap.josm.tools.ImageProvider;
032
033/**
034 * Abstract superclass of relation saving actions (OK, Apply, Cancel).
035 * @since 9496
036 */
037abstract class SavingAction extends AbstractRelationEditorAction {
038
039    protected final TagEditorModel tagModel;
040    protected final AutoCompletingTextField tfRole;
041
042    protected SavingAction(MemberTable memberTable, MemberTableModel memberTableModel, TagEditorModel tagModel, OsmDataLayer layer,
043            IRelationEditor editor, AutoCompletingTextField tfRole) {
044        super(memberTable, memberTableModel, null, layer, editor);
045        this.tagModel = tagModel;
046        this.tfRole = tfRole;
047    }
048
049    /**
050     * apply updates to a new relation
051     * @param tagEditorModel tag editor model
052     */
053    protected void applyNewRelation(TagEditorModel tagEditorModel) {
054        final Relation newRelation = new Relation();
055        tagEditorModel.applyToPrimitive(newRelation);
056        memberTableModel.applyToRelation(newRelation);
057        List<RelationMember> newMembers = new ArrayList<>();
058        for (RelationMember rm: newRelation.getMembers()) {
059            if (!rm.getMember().isDeleted()) {
060                newMembers.add(rm);
061            }
062        }
063        if (newRelation.getMembersCount() != newMembers.size()) {
064            newRelation.setMembers(newMembers);
065            String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" +
066            "was open. They have been removed from the relation members list.");
067            JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE);
068        }
069        // If the user wanted to create a new relation, but hasn't added any members or
070        // tags, don't add an empty relation
071        if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys())
072            return;
073        Main.main.undoRedo.add(new AddCommand(layer, newRelation));
074
075        // make sure everybody is notified about the changes
076        //
077        layer.data.fireSelectionChanged();
078        editor.setRelation(newRelation);
079        if (editor instanceof RelationEditor) {
080            RelationDialogManager.getRelationDialogManager().updateContext(
081                    layer, editor.getRelation(), (RelationEditor) editor);
082        }
083        // Relation list gets update in EDT so selecting my be postponed to following EDT run
084        SwingUtilities.invokeLater(() -> Main.map.relationListDialog.selectRelation(newRelation));
085    }
086
087    /**
088     * Apply the updates for an existing relation which has been changed outside of the relation editor.
089     * @param tagEditorModel tag editor model
090     */
091    protected void applyExistingConflictingRelation(TagEditorModel tagEditorModel) {
092        Relation editedRelation = new Relation(editor.getRelation());
093        tagEditorModel.applyToPrimitive(editedRelation);
094        memberTableModel.applyToRelation(editedRelation);
095        Conflict<Relation> conflict = new Conflict<>(editor.getRelation(), editedRelation);
096        Main.main.undoRedo.add(new ConflictAddCommand(layer, conflict));
097    }
098
099    /**
100     * Apply the updates for an existing relation which has not been changed outside of the relation editor.
101     * @param tagEditorModel tag editor model
102     */
103    protected void applyExistingNonConflictingRelation(TagEditorModel tagEditorModel) {
104        Relation editedRelation = new Relation(editor.getRelation());
105        tagEditorModel.applyToPrimitive(editedRelation);
106        memberTableModel.applyToRelation(editedRelation);
107        Main.main.undoRedo.add(new ChangeCommand(editor.getRelation(), editedRelation));
108        layer.data.fireSelectionChanged();
109    }
110
111    protected boolean confirmClosingBecauseOfDirtyState() {
112        ButtonSpec[] options = new ButtonSpec[] {
113                new ButtonSpec(
114                        tr("Yes, create a conflict and close"),
115                        ImageProvider.get("ok"),
116                        tr("Click to create a conflict and close this relation editor"),
117                        null /* no specific help topic */
118                ),
119                new ButtonSpec(
120                        tr("No, continue editing"),
121                        ImageProvider.get("cancel"),
122                        tr("Click to return to the relation editor and to resume relation editing"),
123                        null /* no specific help topic */
124                )
125        };
126
127        int ret = HelpAwareOptionPane.showOptionDialog(
128                Main.parent,
129                tr("<html>This relation has been changed outside of the editor.<br>"
130                        + "You cannot apply your changes and continue editing.<br>"
131                        + "<br>"
132                        + "Do you want to create a conflict and close the editor?</html>"),
133                        tr("Conflict in data"),
134                        JOptionPane.WARNING_MESSAGE,
135                        null,
136                        options,
137                        options[0], // OK is default
138                        "/Dialog/RelationEditor#RelationChangedOutsideOfEditor"
139        );
140        if (ret == 0) {
141            Main.map.conflictDialog.unfurlDialog();
142        }
143        return ret == 0;
144    }
145
146    protected void warnDoubleConflict() {
147        JOptionPane.showMessageDialog(
148                Main.parent,
149                tr("<html>Layer ''{0}'' already has a conflict for object<br>"
150                        + "''{1}''.<br>"
151                        + "Please resolve this conflict first, then try again.</html>",
152                        layer.getName(),
153                        editor.getRelation().getDisplayName(DefaultNameFormatter.getInstance())
154                ),
155                tr("Double conflict"),
156                JOptionPane.WARNING_MESSAGE
157        );
158    }
159
160    @Override
161    protected void updateEnabledState() {
162        // Do nothing
163    }
164
165    protected boolean applyChanges() {
166        if (editor.getRelation() == null) {
167            applyNewRelation(tagModel);
168        } else if (isEditorDirty()) {
169            if (editor.isDirtyRelation()) {
170                if (confirmClosingBecauseOfDirtyState()) {
171                    if (layer.getConflicts().hasConflictForMy(editor.getRelation())) {
172                        warnDoubleConflict();
173                        return false;
174                    }
175                    applyExistingConflictingRelation(tagModel);
176                    hideEditor();
177                } else
178                    return false;
179            } else {
180                applyExistingNonConflictingRelation(tagModel);
181            }
182        }
183        editor.setRelation(editor.getRelation());
184        return true;
185    }
186
187    protected void hideEditor() {
188        if (editor instanceof Component) {
189            ((Component) editor).setVisible(false);
190        }
191    }
192
193    protected boolean isEditorDirty() {
194        Relation snapshot = editor.getRelationSnapshot();
195        return (snapshot != null && !memberTableModel.hasSameMembersAs(snapshot)) || tagModel.isDirty();
196    }
197}