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