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.JComponent;
015import javax.swing.JPanel;
016import javax.swing.JScrollPane;
017import javax.swing.JTabbedPane;
018
019import org.openstreetmap.josm.command.Command;
020import org.openstreetmap.josm.command.SequenceCommand;
021import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand;
022import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand;
023import org.openstreetmap.josm.data.conflict.Conflict;
024import org.openstreetmap.josm.data.osm.Node;
025import org.openstreetmap.josm.data.osm.OsmPrimitive;
026import org.openstreetmap.josm.data.osm.Relation;
027import org.openstreetmap.josm.data.osm.Way;
028import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger;
029import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel;
030import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger;
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 * @since 1622
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        addTab(tr("Properties"), propertiesMerger);
103
104        tagMerger = new TagMerger();
105        tagMerger.setName("panel.tagmerger");
106        tagMerger.getModel().addPropertyChangeListener(this);
107        addTab(tr("Tags"), tagMerger);
108
109        nodeListMerger = new NodeListMerger();
110        nodeListMerger.setName("panel.nodelistmerger");
111        nodeListMerger.getModel().addPropertyChangeListener(this);
112        addTab(tr("Nodes"), nodeListMerger);
113
114        relationMemberMerger = new RelationMemberMerger();
115        relationMemberMerger.setName("panel.relationmembermerger");
116        relationMemberMerger.getModel().addPropertyChangeListener(this);
117        addTab(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    private void addTab(String title, JComponent tabContent) {
129        JScrollPane scrollPanel = new JScrollPane(tabContent);
130        tabbedPane.add(title, scrollPanel);
131    }
132
133    /**
134     * constructor
135     */
136    public ConflictResolver() {
137        resolvedCompletely = false;
138        build();
139        loadIcons();
140    }
141
142    /**
143     * Sets the {@link OsmPrimitive} in the role "my"
144     *
145     * @param my the primitive in the role "my"
146     */
147    protected void setMy(OsmPrimitive my) {
148        OsmPrimitive old = this.my;
149        this.my = my;
150        if (old != this.my) {
151            firePropertyChange(MY_PRIMITIVE_PROP, old, this.my);
152        }
153    }
154
155    /**
156     * Sets the {@link OsmPrimitive} in the role "their".
157     *
158     * @param their the primitive in the role "their"
159     */
160    protected void setTheir(OsmPrimitive their) {
161        OsmPrimitive old = this.their;
162        this.their = their;
163        if (old != this.their) {
164            firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their);
165        }
166    }
167
168    /**
169     * handles property change events
170     * @param evt the event
171     * @see TagMergeModel
172     * @see AbstractListMergeModel
173     * @see PropertiesMergeModel
174     */
175    @Override
176    public void propertyChange(PropertyChangeEvent evt) {
177        if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
178            int newValue = (Integer) evt.getNewValue();
179            if (newValue == 0) {
180                tabbedPane.setTitleAt(1, tr("Tags"));
181                tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved"));
182                tabbedPane.setIconAt(1, mergeComplete);
183            } else {
184                tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue));
185                tabbedPane.setToolTipTextAt(1,
186                        trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
187                tabbedPane.setIconAt(1, mergeIncomplete);
188            }
189            updateResolvedCompletely();
190        } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
191            boolean frozen = (Boolean) evt.getNewValue();
192            if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
193                if (frozen) {
194                    tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
195                    tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
196                    tabbedPane.setIconAt(2, mergeComplete);
197                } else {
198                    tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
199                    tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way"));
200                    tabbedPane.setIconAt(2, mergeIncomplete);
201                }
202            } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
203                if (frozen) {
204                    tabbedPane.setTitleAt(3, tr("Members(resolved)"));
205                    tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
206                    tabbedPane.setIconAt(3, mergeComplete);
207                } else {
208                    tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
209                    tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
210                    tabbedPane.setIconAt(3, mergeIncomplete);
211                }
212            }
213            updateResolvedCompletely();
214        } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
215            boolean resolved = (Boolean) evt.getNewValue();
216            if (resolved) {
217                tabbedPane.setTitleAt(0, tr("Properties"));
218                tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
219                tabbedPane.setIconAt(0, mergeComplete);
220            } else {
221                tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
222                tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
223                tabbedPane.setIconAt(0, mergeIncomplete);
224            }
225            updateResolvedCompletely();
226        } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
227            for (IConflictResolver resolver: conflictResolvers) {
228                resolver.deletePrimitive((Boolean) evt.getNewValue());
229            }
230        }
231    }
232
233    /**
234     * populates the conflict resolver with the conflicts between my and their
235     *
236     * @param conflict the conflict data set
237     */
238    public void populate(Conflict<? extends OsmPrimitive> conflict) {
239        setMy(conflict.getMy());
240        setTheir(conflict.getTheir());
241        this.conflict = conflict;
242        propertiesMerger.populate(conflict);
243
244        tabbedPane.setEnabledAt(0, true);
245        tagMerger.populate(conflict);
246        tabbedPane.setEnabledAt(1, true);
247
248        if (my instanceof Node) {
249            tabbedPane.setEnabledAt(2, false);
250            tabbedPane.setEnabledAt(3, false);
251        } else if (my instanceof Way) {
252            nodeListMerger.populate(conflict);
253            tabbedPane.setEnabledAt(2, true);
254            tabbedPane.setEnabledAt(3, false);
255            tabbedPane.setTitleAt(3, tr("Members"));
256            tabbedPane.setIconAt(3, null);
257        } else if (my instanceof Relation) {
258            relationMemberMerger.populate(conflict);
259            tabbedPane.setEnabledAt(2, false);
260            tabbedPane.setTitleAt(2, tr("Nodes"));
261            tabbedPane.setIconAt(2, null);
262            tabbedPane.setEnabledAt(3, true);
263        }
264        updateResolvedCompletely();
265        selectFirstTabWithConflicts();
266    }
267
268    /**
269     * {@link JTabbedPane#setSelectedIndex(int) Selects} the first tab with conflicts
270     */
271    public void selectFirstTabWithConflicts() {
272        for (int i = 0; i < tabbedPane.getTabCount(); i++) {
273            if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) {
274                tabbedPane.setSelectedIndex(i);
275                break;
276            }
277        }
278    }
279
280    /**
281     * Builds the resolution command(s) for the resolved conflicts in this ConflictResolver
282     *
283     * @return the resolution command
284     */
285    public Command buildResolveCommand() {
286        List<Command> commands = new ArrayList<>();
287
288        if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
289            commands.add(tagMerger.getModel().buildResolveCommand(conflict));
290        }
291        commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict));
292        if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
293            commands.add(nodeListMerger.getModel().buildResolveCommand(conflict));
294        } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
295            commands.add(relationMemberMerger.getModel().buildResolveCommand(conflict));
296        }
297        if (isResolvedCompletely()) {
298            commands.add(new VersionConflictResolveCommand(conflict));
299            commands.add(new ModifiedConflictResolveCommand(conflict));
300        }
301        return new SequenceCommand(tr("Conflict Resolution"), commands);
302    }
303
304    /**
305     * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP}
306     *
307     */
308    protected void updateResolvedCompletely() {
309        boolean oldValueResolvedCompletely = resolvedCompletely;
310        if (my instanceof Node) {
311            // resolve the version conflict if this is a node and all tag
312            // conflicts have been resolved
313            //
314            this.resolvedCompletely =
315                tagMerger.getModel().isResolvedCompletely()
316                && propertiesMerger.getModel().isResolvedCompletely();
317        } else if (my instanceof Way) {
318            // resolve the version conflict if this is a way, all tag
319            // conflicts have been resolved, and conflicts in the node list
320            // have been resolved
321            //
322            this.resolvedCompletely =
323                tagMerger.getModel().isResolvedCompletely()
324                && propertiesMerger.getModel().isResolvedCompletely()
325                && nodeListMerger.getModel().isFrozen();
326        } else if (my instanceof Relation) {
327            // resolve the version conflict if this is a relation, all tag
328            // conflicts and all conflicts in the member list
329            // have been resolved
330            //
331            this.resolvedCompletely =
332                tagMerger.getModel().isResolvedCompletely()
333                && propertiesMerger.getModel().isResolvedCompletely()
334                && relationMemberMerger.getModel().isFrozen();
335        }
336        if (this.resolvedCompletely != oldValueResolvedCompletely) {
337            firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely);
338        }
339    }
340
341    /**
342     * Replies true all differences in this conflicts are resolved
343     *
344     * @return true all differences in this conflicts are resolved
345     */
346    public boolean isResolvedCompletely() {
347        return resolvedCompletely;
348    }
349
350    /**
351     * Adds all registered listeners by this conflict resolver
352     * @see #unregisterListeners()
353     * @since 10454
354     */
355    public void registerListeners() {
356        nodeListMerger.registerListeners();
357        relationMemberMerger.registerListeners();
358    }
359
360    /**
361     * Removes all registered listeners by this conflict resolver
362     */
363    public void unregisterListeners() {
364        nodeListMerger.unregisterListeners();
365        relationMemberMerger.unregisterListeners();
366    }
367
368    /**
369     * {@link PropertiesMerger#decideRemaining(MergeDecisionType) Decides/resolves} undecided conflicts to the given decision type
370     * @param decision the decision to take for undecided conflicts
371     * @throws AssertionError if {@link #isResolvedCompletely()} does not hold after applying the decision
372     */
373    public void decideRemaining(MergeDecisionType decision) {
374        propertiesMerger.decideRemaining(decision);
375        tagMerger.decideRemaining(decision);
376        if (my instanceof Way) {
377            nodeListMerger.decideRemaining(decision);
378        } else if (my instanceof Relation) {
379            relationMemberMerger.decideRemaining(decision);
380        }
381        updateResolvedCompletely();
382        if (!isResolvedCompletely()) {
383            throw new AssertionError("The conflict could not be resolved completely!");
384        }
385    }
386}