// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.conflict.pair; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.BorderLayout; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.JTabbedPane; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand; import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand; import org.openstreetmap.josm.data.conflict.Conflict; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger; import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel; import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger; import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger; import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel; import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger; import org.openstreetmap.josm.tools.ImageProvider; /** * An UI component for resolving conflicts between two {@link OsmPrimitive}s. * * This component emits {@link PropertyChangeEvent}s for three properties: * <ul> * <li>{@link #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is * completely resolved</li> * <li>{@link #MY_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of * my primitive</li> * <li>{@link #THEIR_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of * their primitive</li> * </ul> * @since 1622 */ public class ConflictResolver extends JPanel implements PropertyChangeListener { /* -------------------------------------------------------------------------------------- */ /* Property names */ /* -------------------------------------------------------------------------------------- */ /** name of the property indicating whether all conflicts are resolved, * {@link #isResolvedCompletely()} */ public static final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely"; /** * name of the property for the {@link OsmPrimitive} in the role "my" */ public static final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive"; /** * name of the property for the {@link OsmPrimitive} in the role "my" */ public static final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive"; private JTabbedPane tabbedPane; private TagMerger tagMerger; private NodeListMerger nodeListMerger; private RelationMemberMerger relationMemberMerger; private PropertiesMerger propertiesMerger; private final transient List<IConflictResolver> conflictResolvers = new ArrayList<>(); private transient OsmPrimitive my; private transient OsmPrimitive their; private transient Conflict<? extends OsmPrimitive> conflict; private ImageIcon mergeComplete; private ImageIcon mergeIncomplete; /** indicates whether the current conflict is resolved completely */ private boolean resolvedCompletely; /** * loads the required icons */ protected final void loadIcons() { mergeComplete = ImageProvider.get("dialogs", "valid"); mergeIncomplete = ImageProvider.get("dialogs/conflict", "mergeincomplete"); } /** * builds the UI */ protected final void build() { tabbedPane = new JTabbedPane(); propertiesMerger = new PropertiesMerger(); propertiesMerger.setName("panel.propertiesmerger"); propertiesMerger.getModel().addPropertyChangeListener(this); tabbedPane.add(tr("Properties"), propertiesMerger); tagMerger = new TagMerger(); tagMerger.setName("panel.tagmerger"); tagMerger.getModel().addPropertyChangeListener(this); tabbedPane.add(tr("Tags"), tagMerger); nodeListMerger = new NodeListMerger(); nodeListMerger.setName("panel.nodelistmerger"); nodeListMerger.getModel().addPropertyChangeListener(this); tabbedPane.add(tr("Nodes"), nodeListMerger); relationMemberMerger = new RelationMemberMerger(); relationMemberMerger.setName("panel.relationmembermerger"); relationMemberMerger.getModel().addPropertyChangeListener(this); tabbedPane.add(tr("Members"), relationMemberMerger); setLayout(new BorderLayout()); add(tabbedPane, BorderLayout.CENTER); conflictResolvers.add(propertiesMerger); conflictResolvers.add(tagMerger); conflictResolvers.add(nodeListMerger); conflictResolvers.add(relationMemberMerger); } /** * constructor */ public ConflictResolver() { resolvedCompletely = false; build(); loadIcons(); } /** * Sets the {@link OsmPrimitive} in the role "my" * * @param my the primitive in the role "my" */ protected void setMy(OsmPrimitive my) { OsmPrimitive old = this.my; this.my = my; if (old != this.my) { firePropertyChange(MY_PRIMITIVE_PROP, old, this.my); } } /** * Sets the {@link OsmPrimitive} in the role "their". * * @param their the primitive in the role "their" */ protected void setTheir(OsmPrimitive their) { OsmPrimitive old = this.their; this.their = their; if (old != this.their) { firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their); } } /** * handles property change events * @param evt the event * @see TagMergeModel * @see AbstractListMergeModel * @see PropertiesMergeModel */ @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) { int newValue = (Integer) evt.getNewValue(); if (newValue == 0) { tabbedPane.setTitleAt(1, tr("Tags")); tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved")); tabbedPane.setIconAt(1, mergeComplete); } else { tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue)); tabbedPane.setToolTipTextAt(1, trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue)); tabbedPane.setIconAt(1, mergeIncomplete); } updateResolvedCompletely(); } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) { boolean frozen = (Boolean) evt.getNewValue(); if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) { if (frozen) { tabbedPane.setTitleAt(2, tr("Nodes(resolved)")); tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way")); tabbedPane.setIconAt(2, mergeComplete); } else { tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)")); tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way")); tabbedPane.setIconAt(2, mergeIncomplete); } } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) { if (frozen) { tabbedPane.setTitleAt(3, tr("Members(resolved)")); tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation")); tabbedPane.setIconAt(3, mergeComplete); } else { tabbedPane.setTitleAt(3, tr("Members(with conflicts)")); tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation")); tabbedPane.setIconAt(3, mergeIncomplete); } } updateResolvedCompletely(); } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) { boolean resolved = (Boolean) evt.getNewValue(); if (resolved) { tabbedPane.setTitleAt(0, tr("Properties")); tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts")); tabbedPane.setIconAt(0, mergeComplete); } else { tabbedPane.setTitleAt(0, tr("Properties(with conflicts)")); tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved")); tabbedPane.setIconAt(0, mergeIncomplete); } updateResolvedCompletely(); } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) { for (IConflictResolver resolver: conflictResolvers) { resolver.deletePrimitive((Boolean) evt.getNewValue()); } } } /** * populates the conflict resolver with the conflicts between my and their * * @param conflict the conflict data set */ public void populate(Conflict<? extends OsmPrimitive> conflict) { setMy(conflict.getMy()); setTheir(conflict.getTheir()); this.conflict = conflict; propertiesMerger.populate(conflict); tabbedPane.setEnabledAt(0, true); tagMerger.populate(conflict); tabbedPane.setEnabledAt(1, true); if (my instanceof Node) { tabbedPane.setEnabledAt(2, false); tabbedPane.setEnabledAt(3, false); } else if (my instanceof Way) { nodeListMerger.populate(conflict); tabbedPane.setEnabledAt(2, true); tabbedPane.setEnabledAt(3, false); tabbedPane.setTitleAt(3, tr("Members")); tabbedPane.setIconAt(3, null); } else if (my instanceof Relation) { relationMemberMerger.populate(conflict); tabbedPane.setEnabledAt(2, false); tabbedPane.setTitleAt(2, tr("Nodes")); tabbedPane.setIconAt(2, null); tabbedPane.setEnabledAt(3, true); } updateResolvedCompletely(); selectFirstTabWithConflicts(); } /** * {@link JTabbedPane#setSelectedIndex(int) Selects} the first tab with conflicts */ public void selectFirstTabWithConflicts() { for (int i = 0; i < tabbedPane.getTabCount(); i++) { if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) { tabbedPane.setSelectedIndex(i); break; } } } /** * Builds the resolution command(s) for the resolved conflicts in this ConflictResolver * * @return the resolution command */ public Command buildResolveCommand() { List<Command> commands = new ArrayList<>(); if (tagMerger.getModel().getNumResolvedConflicts() > 0) { commands.add(tagMerger.getModel().buildResolveCommand(conflict)); } commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict)); if (my instanceof Way && nodeListMerger.getModel().isFrozen()) { commands.add(nodeListMerger.getModel().buildResolveCommand(conflict)); } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) { commands.add(relationMemberMerger.getModel().buildResolveCommand(conflict)); } if (isResolvedCompletely()) { commands.add(new VersionConflictResolveCommand(conflict)); commands.add(new ModifiedConflictResolveCommand(conflict)); } return new SequenceCommand(tr("Conflict Resolution"), commands); } /** * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP} * */ protected void updateResolvedCompletely() { boolean oldValueResolvedCompletely = resolvedCompletely; if (my instanceof Node) { // resolve the version conflict if this is a node and all tag // conflicts have been resolved // this.resolvedCompletely = tagMerger.getModel().isResolvedCompletely() && propertiesMerger.getModel().isResolvedCompletely(); } else if (my instanceof Way) { // resolve the version conflict if this is a way, all tag // conflicts have been resolved, and conflicts in the node list // have been resolved // this.resolvedCompletely = tagMerger.getModel().isResolvedCompletely() && propertiesMerger.getModel().isResolvedCompletely() && nodeListMerger.getModel().isFrozen(); } else if (my instanceof Relation) { // resolve the version conflict if this is a relation, all tag // conflicts and all conflicts in the member list // have been resolved // this.resolvedCompletely = tagMerger.getModel().isResolvedCompletely() && propertiesMerger.getModel().isResolvedCompletely() && relationMemberMerger.getModel().isFrozen(); } if (this.resolvedCompletely != oldValueResolvedCompletely) { firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely); } } /** * Replies true all differences in this conflicts are resolved * * @return true all differences in this conflicts are resolved */ public boolean isResolvedCompletely() { return resolvedCompletely; } /** * Adds all registered listeners by this conflict resolver * @see #unregisterListeners() * @since 10454 */ public void registerListeners() { nodeListMerger.registerListeners(); relationMemberMerger.registerListeners(); } /** * Removes all registered listeners by this conflict resolver */ public void unregisterListeners() { nodeListMerger.unregisterListeners(); relationMemberMerger.unregisterListeners(); } /** * {@link PropertiesMerger#decideRemaining(MergeDecisionType) Decides/resolves} undecided conflicts to the given decision type * @param decision the decision to take for undecided conflicts * @throws AssertionError if {@link #isResolvedCompletely()} does not hold after applying the decision */ public void decideRemaining(MergeDecisionType decision) { propertiesMerger.decideRemaining(decision); tagMerger.decideRemaining(decision); if (my instanceof Way) { nodeListMerger.decideRemaining(decision); } else if (my instanceof Relation) { relationMemberMerger.decideRemaining(decision); } updateResolvedCompletely(); if (!isResolvedCompletely()) { throw new AssertionError("The conflict could not be resolved completely!"); } } }