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.ModifiedConflictResolveCommand;
019import org.openstreetmap.josm.command.SequenceCommand;
020import org.openstreetmap.josm.command.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    static public 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    static public 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    static public final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive";
068
069    private JTabbedPane tabbedPane = null;
070    private TagMerger tagMerger;
071    private NodeListMerger nodeListMerger;
072    private RelationMemberMerger relationMemberMerger;
073    private PropertiesMerger propertiesMerger;
074    private final List<IConflictResolver> conflictResolvers = new ArrayList<IConflictResolver>();
075    private OsmPrimitive my;
076    private OsmPrimitive their;
077    private 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 void loadIcons() {
089        mergeComplete = ImageProvider.get("dialogs/conflict","mergecomplete.png" );
090        mergeIncomplete = ImageProvider.get("dialogs/conflict","mergeincomplete.png" );
091    }
092
093    /**
094     * builds the UI
095     */
096    protected 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, trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
181                tabbedPane.setIconAt(1, mergeIncomplete);
182            }
183            updateResolvedCompletely();
184        } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
185            boolean frozen = (Boolean)evt.getNewValue();
186            if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
187                if (frozen) {
188                    tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
189                    tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
190                    tabbedPane.setIconAt(2, mergeComplete);
191                } else {
192                    tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
193                    tabbedPane.setToolTipTextAt(2,tr("Pending conflicts in the node list of this way"));
194                    tabbedPane.setIconAt(2, mergeIncomplete);
195                }
196            } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
197                if (frozen) {
198                    tabbedPane.setTitleAt(3, tr("Members(resolved)"));
199                    tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
200                    tabbedPane.setIconAt(3, mergeComplete);
201                } else {
202                    tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
203                    tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
204                    tabbedPane.setIconAt(3, mergeIncomplete);
205                }
206            }
207            updateResolvedCompletely();
208        } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
209            boolean resolved = (Boolean)evt.getNewValue();
210            if (resolved) {
211                tabbedPane.setTitleAt(0, tr("Properties"));
212                tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
213                tabbedPane.setIconAt(0, mergeComplete);
214            } else {
215                tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
216                tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
217                tabbedPane.setIconAt(0, mergeIncomplete);
218            }
219            updateResolvedCompletely();
220        } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
221            for (IConflictResolver resolver: conflictResolvers) {
222                resolver.deletePrimitive((Boolean) evt.getNewValue());
223            }
224        }
225    }
226
227    /**
228     * populates the conflict resolver with the conflicts between my and their
229     *
230     * @param conflict the conflict data set
231     */
232    public void populate(Conflict<? extends OsmPrimitive> conflict) {
233        setMy(conflict.getMy());
234        setTheir(conflict.getTheir());
235        this.conflict = conflict;
236        propertiesMerger.populate(conflict);
237
238        tabbedPane.setEnabledAt(0, true);
239        tagMerger.populate(conflict);
240        tabbedPane.setEnabledAt(1, true);
241
242        if (my instanceof Node) {
243            tabbedPane.setEnabledAt(2,false);
244            tabbedPane.setEnabledAt(3,false);
245        } else if (my instanceof Way) {
246            nodeListMerger.populate(conflict);
247            tabbedPane.setEnabledAt(2, true);
248            tabbedPane.setEnabledAt(3, false);
249            tabbedPane.setTitleAt(3,tr("Members"));
250            tabbedPane.setIconAt(3, null);
251        } else if (my instanceof Relation) {
252            relationMemberMerger.populate(conflict);
253            tabbedPane.setEnabledAt(2, false);
254            tabbedPane.setTitleAt(2,tr("Nodes"));
255            tabbedPane.setIconAt(2, null);
256            tabbedPane.setEnabledAt(3, true);
257        }
258        updateResolvedCompletely();
259        selectFirstTabWithConflicts();
260    }
261
262    public void selectFirstTabWithConflicts() {
263        for (int i = 0; i < tabbedPane.getTabCount(); i++) {
264            if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) {
265                tabbedPane.setSelectedIndex(i);
266                break;
267            }
268        }
269    }
270
271    /**
272     * Builds the resolution command(s) for the resolved conflicts in this
273     * ConflictResolver
274     *
275     * @return the resolution command
276     */
277    public Command buildResolveCommand() {
278        List<Command> commands = new ArrayList<Command>();
279
280        if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
281            commands.add(tagMerger.getModel().buildResolveCommand(conflict));
282        }
283        commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict));
284        if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
285            NodeListMergeModel model = (NodeListMergeModel) nodeListMerger.getModel();
286            commands.add(model.buildResolveCommand(conflict));
287        } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
288            RelationMemberListMergeModel model = (RelationMemberListMergeModel) relationMemberMerger.getModel();
289            commands.add(model.buildResolveCommand((Relation) my, (Relation) their));
290        }
291        if (isResolvedCompletely()) {
292            commands.add(new VersionConflictResolveCommand(conflict));
293            commands.add(new ModifiedConflictResolveCommand(conflict));
294        }
295        return new SequenceCommand(tr("Conflict Resolution"), commands);
296    }
297
298    /**
299     * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP}
300     *
301     */
302    protected void updateResolvedCompletely() {
303        boolean oldValueResolvedCompletely = resolvedCompletely;
304        if (my instanceof Node) {
305            // resolve the version conflict if this is a node and all tag
306            // conflicts have been resolved
307            //
308            this.resolvedCompletely =
309                tagMerger.getModel().isResolvedCompletely()
310                && propertiesMerger.getModel().isResolvedCompletely();
311        } else if (my instanceof Way) {
312            // resolve the version conflict if this is a way, all tag
313            // conflicts have been resolved, and conflicts in the node list
314            // have been resolved
315            //
316            this.resolvedCompletely =
317                tagMerger.getModel().isResolvedCompletely()
318                &&  propertiesMerger.getModel().isResolvedCompletely()
319                && nodeListMerger.getModel().isFrozen();
320        }  else if (my instanceof Relation) {
321            // resolve the version conflict if this is a relation, all tag
322            // conflicts and all conflicts in the member list
323            // have been resolved
324            //
325            this.resolvedCompletely =
326                tagMerger.getModel().isResolvedCompletely()
327                &&  propertiesMerger.getModel().isResolvedCompletely()
328                && relationMemberMerger.getModel().isFrozen();
329        }
330        if (this.resolvedCompletely != oldValueResolvedCompletely) {
331            firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely);
332        }
333    }
334
335    /**
336     * Replies true all differences in this conflicts are resolved
337     *
338     * @return true all differences in this conflicts are resolved
339     */
340    public boolean isResolvedCompletely() {
341        return resolvedCompletely;
342    }
343
344    public void unregisterListeners() {
345        nodeListMerger.unlinkAsListener();
346        relationMemberMerger.unlinkAsListener();
347    }
348}