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