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("misc", "green_check");
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        build();
138        loadIcons();
139    }
140
141    /**
142     * Sets the {@link OsmPrimitive} in the role "my"
143     *
144     * @param my the primitive in the role "my"
145     */
146    protected void setMy(OsmPrimitive my) {
147        OsmPrimitive old = this.my;
148        this.my = my;
149        if (old != this.my) {
150            firePropertyChange(MY_PRIMITIVE_PROP, old, this.my);
151        }
152    }
153
154    /**
155     * Sets the {@link OsmPrimitive} in the role "their".
156     *
157     * @param their the primitive in the role "their"
158     */
159    protected void setTheir(OsmPrimitive their) {
160        OsmPrimitive old = this.their;
161        this.their = their;
162        if (old != this.their) {
163            firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their);
164        }
165    }
166
167    /**
168     * handles property change events
169     * @param evt the event
170     * @see TagMergeModel
171     * @see AbstractListMergeModel
172     * @see PropertiesMergeModel
173     */
174    @Override
175    public void propertyChange(PropertyChangeEvent evt) {
176        if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
177            int newValue = (Integer) evt.getNewValue();
178            if (newValue == 0) {
179                tabbedPane.setTitleAt(1, tr("Tags"));
180                tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved"));
181                tabbedPane.setIconAt(1, mergeComplete);
182            } else {
183                tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue));
184                tabbedPane.setToolTipTextAt(1,
185                        trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
186                tabbedPane.setIconAt(1, mergeIncomplete);
187            }
188            updateResolvedCompletely();
189        } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
190            boolean frozen = (Boolean) evt.getNewValue();
191            if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
192                if (frozen) {
193                    tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
194                    tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
195                    tabbedPane.setIconAt(2, mergeComplete);
196                } else {
197                    tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
198                    tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way"));
199                    tabbedPane.setIconAt(2, mergeIncomplete);
200                }
201            } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
202                if (frozen) {
203                    tabbedPane.setTitleAt(3, tr("Members(resolved)"));
204                    tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
205                    tabbedPane.setIconAt(3, mergeComplete);
206                } else {
207                    tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
208                    tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
209                    tabbedPane.setIconAt(3, mergeIncomplete);
210                }
211            }
212            updateResolvedCompletely();
213        } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
214            boolean resolved = (Boolean) evt.getNewValue();
215            if (resolved) {
216                tabbedPane.setTitleAt(0, tr("Properties"));
217                tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
218                tabbedPane.setIconAt(0, mergeComplete);
219            } else {
220                tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
221                tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
222                tabbedPane.setIconAt(0, mergeIncomplete);
223            }
224            updateResolvedCompletely();
225        } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
226            for (IConflictResolver resolver: conflictResolvers) {
227                resolver.deletePrimitive((Boolean) evt.getNewValue());
228            }
229        }
230    }
231
232    /**
233     * populates the conflict resolver with the conflicts between my and their
234     *
235     * @param conflict the conflict data set
236     */
237    public void populate(Conflict<? extends OsmPrimitive> conflict) {
238        setMy(conflict.getMy());
239        setTheir(conflict.getTheir());
240        this.conflict = conflict;
241        this.resolvedCompletely = false;
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}