001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.pair;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.BorderLayout;
008    import java.beans.PropertyChangeEvent;
009    import java.beans.PropertyChangeListener;
010    import java.util.ArrayList;
011    import java.util.List;
012    
013    import javax.swing.ImageIcon;
014    import javax.swing.JPanel;
015    import javax.swing.JTabbedPane;
016    
017    import org.openstreetmap.josm.command.Command;
018    import org.openstreetmap.josm.command.ModifiedConflictResolveCommand;
019    import org.openstreetmap.josm.command.SequenceCommand;
020    import org.openstreetmap.josm.command.VersionConflictResolveCommand;
021    import org.openstreetmap.josm.data.conflict.Conflict;
022    import org.openstreetmap.josm.data.osm.Node;
023    import org.openstreetmap.josm.data.osm.OsmPrimitive;
024    import org.openstreetmap.josm.data.osm.Relation;
025    import org.openstreetmap.josm.data.osm.Way;
026    import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMergeModel;
027    import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger;
028    import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel;
029    import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger;
030    import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberListMergeModel;
031    import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger;
032    import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel;
033    import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger;
034    import 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     */
050    public 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         */
166        public void propertyChange(PropertyChangeEvent evt) {
167            if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
168                int newValue = (Integer)evt.getNewValue();
169                if (newValue == 0) {
170                    tabbedPane.setTitleAt(1, tr("Tags"));
171                    tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved"));
172                    tabbedPane.setIconAt(1, mergeComplete);
173                } else {
174                    tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue));
175                    tabbedPane.setToolTipTextAt(1, trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
176                    tabbedPane.setIconAt(1, mergeIncomplete);
177                }
178                updateResolvedCompletely();
179            } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
180                boolean frozen = (Boolean)evt.getNewValue();
181                if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
182                    if (frozen) {
183                        tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
184                        tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
185                        tabbedPane.setIconAt(2, mergeComplete);
186                    } else {
187                        tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
188                        tabbedPane.setToolTipTextAt(2,tr("Pending conflicts in the node list of this way"));
189                        tabbedPane.setIconAt(2, mergeIncomplete);
190                    }
191                } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
192                    if (frozen) {
193                        tabbedPane.setTitleAt(3, tr("Members(resolved)"));
194                        tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
195                        tabbedPane.setIconAt(3, mergeComplete);
196                    } else {
197                        tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
198                        tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
199                        tabbedPane.setIconAt(3, mergeIncomplete);
200                    }
201                }
202                updateResolvedCompletely();
203            } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
204                boolean resolved = (Boolean)evt.getNewValue();
205                if (resolved) {
206                    tabbedPane.setTitleAt(0, tr("Properties"));
207                    tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
208                    tabbedPane.setIconAt(0, mergeComplete);
209                } else {
210                    tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
211                    tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
212                    tabbedPane.setIconAt(0, mergeIncomplete);
213                }
214                updateResolvedCompletely();
215            } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
216                for (IConflictResolver resolver: conflictResolvers) {
217                    resolver.deletePrimitive((Boolean) evt.getNewValue());
218                }
219            }
220        }
221    
222        /**
223         * populates the conflict resolver with the conflicts between my and their
224         *
225         * @param my   my primitive (i.e. the primitive in the local dataset)
226         * @param their their primitive (i.e. the primitive in the server dataset)
227         *
228         */
229        public void populate(Conflict<? extends OsmPrimitive> conflict) {
230            setMy(conflict.getMy());
231            setTheir(conflict.getTheir());
232            this.conflict = conflict;
233            propertiesMerger.populate(conflict);
234    
235            tabbedPane.setEnabledAt(0, true);
236            tagMerger.populate(conflict);
237            tabbedPane.setEnabledAt(1, true);
238    
239            if (my instanceof Node) {
240                tabbedPane.setEnabledAt(2,false);
241                tabbedPane.setEnabledAt(3,false);
242            } else if (my instanceof Way) {
243                nodeListMerger.populate(conflict);
244                tabbedPane.setEnabledAt(2, true);
245                tabbedPane.setEnabledAt(3, false);
246                tabbedPane.setTitleAt(3,tr("Members"));
247                tabbedPane.setIconAt(3, null);
248            } else if (my instanceof Relation) {
249                relationMemberMerger.populate(conflict);
250                tabbedPane.setEnabledAt(2, false);
251                tabbedPane.setTitleAt(2,tr("Nodes"));
252                tabbedPane.setIconAt(2, null);
253                tabbedPane.setEnabledAt(3, true);
254            }
255            updateResolvedCompletely();
256            selectFirstTabWithConflicts();
257        }
258        
259        public void selectFirstTabWithConflicts() {
260            for (int i = 0; i < tabbedPane.getTabCount(); i++) {
261                if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) {
262                    tabbedPane.setSelectedIndex(i);
263                    break;
264                }
265            }
266        }
267    
268        /**
269         * Builds the resolution command(s) for the resolved conflicts in this
270         * ConflictResolver
271         *
272         * @return the resolution command
273         */
274        public Command buildResolveCommand() {
275            ArrayList<Command> commands = new ArrayList<Command>();
276    
277            if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
278                commands.add(tagMerger.getModel().buildResolveCommand(conflict));
279            }
280            commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict));
281            if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
282                NodeListMergeModel model = (NodeListMergeModel) nodeListMerger.getModel();
283                commands.add(model.buildResolveCommand(conflict));
284            } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
285                RelationMemberListMergeModel model = (RelationMemberListMergeModel) relationMemberMerger.getModel();
286                commands.add(model.buildResolveCommand((Relation) my, (Relation) their));
287            }
288            if (isResolvedCompletely()) {
289                commands.add(new VersionConflictResolveCommand(conflict));
290                commands.add(new ModifiedConflictResolveCommand(conflict));
291            }
292            return new SequenceCommand(tr("Conflict Resolution"), commands);
293        }
294    
295        /**
296         * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP}
297         *
298         */
299        protected void updateResolvedCompletely() {
300            boolean oldValueResolvedCompletely = resolvedCompletely;
301            if (my instanceof Node) {
302                // resolve the version conflict if this is a node and all tag
303                // conflicts have been resolved
304                //
305                this.resolvedCompletely =
306                    tagMerger.getModel().isResolvedCompletely()
307                    && propertiesMerger.getModel().isResolvedCompletely();
308            } else if (my instanceof Way) {
309                // resolve the version conflict if this is a way, all tag
310                // conflicts have been resolved, and conflicts in the node list
311                // have been resolved
312                //
313                this.resolvedCompletely =
314                    tagMerger.getModel().isResolvedCompletely()
315                    &&  propertiesMerger.getModel().isResolvedCompletely()
316                    && nodeListMerger.getModel().isFrozen();
317            }  else if (my instanceof Relation) {
318                // resolve the version conflict if this is a relation, all tag
319                // conflicts and all conflicts in the member list
320                // have been resolved
321                //
322                this.resolvedCompletely =
323                    tagMerger.getModel().isResolvedCompletely()
324                    &&  propertiesMerger.getModel().isResolvedCompletely()
325                    && relationMemberMerger.getModel().isFrozen();
326            }
327            if (this.resolvedCompletely != oldValueResolvedCompletely) {
328                firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely);
329            }
330        }
331    
332        /**
333         * Replies true all differences in this conflicts are resolved
334         *
335         * @return true all differences in this conflicts are resolved
336         */
337        public boolean isResolvedCompletely() {
338            return resolvedCompletely;
339        }
340    
341        public void unregisterListeners() {
342            nodeListMerger.unlinkAsListener();
343            relationMemberMerger.unlinkAsListener();
344        }
345    }