001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.beans.PropertyChangeEvent;
009import java.beans.PropertyChangeListener;
010import java.util.ArrayList;
011import java.util.List;
012
013import javax.swing.ImageIcon;
014import javax.swing.JPanel;
015import javax.swing.JTabbedPane;
016
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.command.SequenceCommand;
019import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand;
020import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand;
021import org.openstreetmap.josm.data.conflict.Conflict;
022import org.openstreetmap.josm.data.osm.Node;
023import org.openstreetmap.josm.data.osm.OsmPrimitive;
024import org.openstreetmap.josm.data.osm.Relation;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMergeModel;
027import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger;
028import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel;
029import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger;
030import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberListMergeModel;
031import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger;
032import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel;
033import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger;
034import org.openstreetmap.josm.tools.ImageProvider;
035
036/**
037 * An UI component for resolving conflicts between two {@link OsmPrimitive}s.
038 *
039 * This component emits {@link PropertyChangeEvent}s for three properties:
040 * <ul>
041 *   <li>{@link #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is
042 *   completely resolved</li>
043 *   <li>{@link #MY_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of
044 *   my primitive</li>
045 *   <li>{@link #THEIR_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of
046 *   their primitive</li>
047 * </ul>
048 *
049 */
050public class ConflictResolver extends JPanel implements PropertyChangeListener  {
051
052    /* -------------------------------------------------------------------------------------- */
053    /* Property names                                                                         */
054    /* -------------------------------------------------------------------------------------- */
055    /** name of the property indicating whether all conflicts are resolved,
056     *  {@link #isResolvedCompletely()}
057     */
058    public static final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely";
059    /**
060     * name of the property for the {@link OsmPrimitive} in the role "my"
061     */
062    public static final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive";
063
064    /**
065     * name of the property for the {@link OsmPrimitive} in the role "my"
066     */
067    public static final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive";
068
069    private JTabbedPane tabbedPane;
070    private TagMerger tagMerger;
071    private NodeListMerger nodeListMerger;
072    private RelationMemberMerger relationMemberMerger;
073    private PropertiesMerger propertiesMerger;
074    private final transient List<IConflictResolver> conflictResolvers = new ArrayList<>();
075    private transient OsmPrimitive my;
076    private transient OsmPrimitive their;
077    private transient Conflict<? extends OsmPrimitive> conflict;
078
079    private ImageIcon mergeComplete;
080    private ImageIcon mergeIncomplete;
081
082    /** indicates whether the current conflict is resolved completely */
083    private boolean resolvedCompletely;
084
085    /**
086     * loads the required icons
087     */
088    protected final void loadIcons() {
089        mergeComplete = ImageProvider.get("dialogs", "valid");
090        mergeIncomplete = ImageProvider.get("dialogs/conflict", "mergeincomplete");
091    }
092
093    /**
094     * builds the UI
095     */
096    protected final void build() {
097        tabbedPane = new JTabbedPane();
098
099        propertiesMerger = new PropertiesMerger();
100        propertiesMerger.setName("panel.propertiesmerger");
101        propertiesMerger.getModel().addPropertyChangeListener(this);
102        tabbedPane.add(tr("Properties"), propertiesMerger);
103
104        tagMerger = new TagMerger();
105        tagMerger.setName("panel.tagmerger");
106        tagMerger.getModel().addPropertyChangeListener(this);
107        tabbedPane.add(tr("Tags"), tagMerger);
108
109        nodeListMerger = new NodeListMerger();
110        nodeListMerger.setName("panel.nodelistmerger");
111        nodeListMerger.getModel().addPropertyChangeListener(this);
112        tabbedPane.add(tr("Nodes"), nodeListMerger);
113
114        relationMemberMerger = new RelationMemberMerger();
115        relationMemberMerger.setName("panel.relationmembermerger");
116        relationMemberMerger.getModel().addPropertyChangeListener(this);
117        tabbedPane.add(tr("Members"), relationMemberMerger);
118
119        setLayout(new BorderLayout());
120        add(tabbedPane, BorderLayout.CENTER);
121
122        conflictResolvers.add(propertiesMerger);
123        conflictResolvers.add(tagMerger);
124        conflictResolvers.add(nodeListMerger);
125        conflictResolvers.add(relationMemberMerger);
126    }
127
128    /**
129     * constructor
130     */
131    public ConflictResolver() {
132        resolvedCompletely = false;
133        build();
134        loadIcons();
135    }
136
137    /**
138     * Sets the {@link OsmPrimitive} in the role "my"
139     *
140     * @param my the primitive in the role "my"
141     */
142    protected void setMy(OsmPrimitive my) {
143        OsmPrimitive old = this.my;
144        this.my = my;
145        if (old != this.my) {
146            firePropertyChange(MY_PRIMITIVE_PROP, old, this.my);
147        }
148    }
149
150    /**
151     * Sets the {@link OsmPrimitive} in the role "their".
152     *
153     * @param their the primitive in the role "their"
154     */
155    protected void setTheir(OsmPrimitive their) {
156        OsmPrimitive old = this.their;
157        this.their = their;
158        if (old != this.their) {
159            firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their);
160        }
161    }
162
163    /**
164     * handles property change events
165     * @param evt the event
166     * @see TagMergeModel
167     * @see ListMergeModel
168     * @see PropertiesMergeModel
169     */
170    @Override
171    public void propertyChange(PropertyChangeEvent evt) {
172        if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
173            int newValue = (Integer) evt.getNewValue();
174            if (newValue == 0) {
175                tabbedPane.setTitleAt(1, tr("Tags"));
176                tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved"));
177                tabbedPane.setIconAt(1, mergeComplete);
178            } else {
179                tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue));
180                tabbedPane.setToolTipTextAt(1,
181                        trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
182                tabbedPane.setIconAt(1, mergeIncomplete);
183            }
184            updateResolvedCompletely();
185        } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
186            boolean frozen = (Boolean) evt.getNewValue();
187            if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
188                if (frozen) {
189                    tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
190                    tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
191                    tabbedPane.setIconAt(2, mergeComplete);
192                } else {
193                    tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
194                    tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way"));
195                    tabbedPane.setIconAt(2, mergeIncomplete);
196                }
197            } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
198                if (frozen) {
199                    tabbedPane.setTitleAt(3, tr("Members(resolved)"));
200                    tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
201                    tabbedPane.setIconAt(3, mergeComplete);
202                } else {
203                    tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
204                    tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
205                    tabbedPane.setIconAt(3, mergeIncomplete);
206                }
207            }
208            updateResolvedCompletely();
209        } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
210            boolean resolved = (Boolean) evt.getNewValue();
211            if (resolved) {
212                tabbedPane.setTitleAt(0, tr("Properties"));
213                tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
214                tabbedPane.setIconAt(0, mergeComplete);
215            } else {
216                tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
217                tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
218                tabbedPane.setIconAt(0, mergeIncomplete);
219            }
220            updateResolvedCompletely();
221        } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
222            for (IConflictResolver resolver: conflictResolvers) {
223                resolver.deletePrimitive((Boolean) evt.getNewValue());
224            }
225        }
226    }
227
228    /**
229     * populates the conflict resolver with the conflicts between my and their
230     *
231     * @param conflict the conflict data set
232     */
233    public void populate(Conflict<? extends OsmPrimitive> conflict) {
234        setMy(conflict.getMy());
235        setTheir(conflict.getTheir());
236        this.conflict = conflict;
237        propertiesMerger.populate(conflict);
238
239        tabbedPane.setEnabledAt(0, true);
240        tagMerger.populate(conflict);
241        tabbedPane.setEnabledAt(1, true);
242
243        if (my instanceof Node) {
244            tabbedPane.setEnabledAt(2, false);
245            tabbedPane.setEnabledAt(3, false);
246        } else if (my instanceof Way) {
247            nodeListMerger.populate(conflict);
248            tabbedPane.setEnabledAt(2, true);
249            tabbedPane.setEnabledAt(3, false);
250            tabbedPane.setTitleAt(3, tr("Members"));
251            tabbedPane.setIconAt(3, null);
252        } else if (my instanceof Relation) {
253            relationMemberMerger.populate(conflict);
254            tabbedPane.setEnabledAt(2, false);
255            tabbedPane.setTitleAt(2, tr("Nodes"));
256            tabbedPane.setIconAt(2, null);
257            tabbedPane.setEnabledAt(3, true);
258        }
259        updateResolvedCompletely();
260        selectFirstTabWithConflicts();
261    }
262
263    public void selectFirstTabWithConflicts() {
264        for (int i = 0; i < tabbedPane.getTabCount(); i++) {
265            if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) {
266                tabbedPane.setSelectedIndex(i);
267                break;
268            }
269        }
270    }
271
272    /**
273     * Builds the resolution command(s) for the resolved conflicts in this
274     * ConflictResolver
275     *
276     * @return the resolution command
277     */
278    public Command buildResolveCommand() {
279        List<Command> commands = new ArrayList<>();
280
281        if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
282            commands.add(tagMerger.getModel().buildResolveCommand(conflict));
283        }
284        commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict));
285        if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
286            NodeListMergeModel model = (NodeListMergeModel) nodeListMerger.getModel();
287            commands.add(model.buildResolveCommand(conflict));
288        } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
289            RelationMemberListMergeModel model = (RelationMemberListMergeModel) relationMemberMerger.getModel();
290            commands.add(model.buildResolveCommand((Relation) my, (Relation) their));
291        }
292        if (isResolvedCompletely()) {
293            commands.add(new VersionConflictResolveCommand(conflict));
294            commands.add(new ModifiedConflictResolveCommand(conflict));
295        }
296        return new SequenceCommand(tr("Conflict Resolution"), commands);
297    }
298
299    /**
300     * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP}
301     *
302     */
303    protected void updateResolvedCompletely() {
304        boolean oldValueResolvedCompletely = resolvedCompletely;
305        if (my instanceof Node) {
306            // resolve the version conflict if this is a node and all tag
307            // conflicts have been resolved
308            //
309            this.resolvedCompletely =
310                tagMerger.getModel().isResolvedCompletely()
311                && propertiesMerger.getModel().isResolvedCompletely();
312        } else if (my instanceof Way) {
313            // resolve the version conflict if this is a way, all tag
314            // conflicts have been resolved, and conflicts in the node list
315            // have been resolved
316            //
317            this.resolvedCompletely =
318                tagMerger.getModel().isResolvedCompletely()
319                &&  propertiesMerger.getModel().isResolvedCompletely()
320                && nodeListMerger.getModel().isFrozen();
321        }  else if (my instanceof Relation) {
322            // resolve the version conflict if this is a relation, all tag
323            // conflicts and all conflicts in the member list
324            // have been resolved
325            //
326            this.resolvedCompletely =
327                tagMerger.getModel().isResolvedCompletely()
328                &&  propertiesMerger.getModel().isResolvedCompletely()
329                && relationMemberMerger.getModel().isFrozen();
330        }
331        if (this.resolvedCompletely != oldValueResolvedCompletely) {
332            firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely);
333        }
334    }
335
336    /**
337     * Replies true all differences in this conflicts are resolved
338     *
339     * @return true all differences in this conflicts are resolved
340     */
341    public boolean isResolvedCompletely() {
342        return resolvedCompletely;
343    }
344
345    public void unregisterListeners() {
346        nodeListMerger.unlinkAsListener();
347        relationMemberMerger.unlinkAsListener();
348    }
349}