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