Index: trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java	(revision 1954)
@@ -13,5 +13,5 @@
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.gui.conflict.MergeDecisionType;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
 import org.openstreetmap.josm.tools.ImageProvider;
 
Index: trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java	(revision 1954)
@@ -12,5 +12,5 @@
 import org.openstreetmap.josm.data.conflict.Conflict;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.gui.conflict.MergeDecisionType;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.tools.ImageProvider;
Index: trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 1954)
@@ -15,6 +15,6 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.gui.conflict.MergeDecisionType;
-import org.openstreetmap.josm.gui.conflict.tags.TagMergeItem;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeItem;
 import org.openstreetmap.josm.tools.ImageProvider;
 
Index: trunk/src/org/openstreetmap/josm/gui/OptionPaneUtil.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/OptionPaneUtil.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/gui/OptionPaneUtil.java	(revision 1954)
@@ -10,5 +10,4 @@
 import javax.swing.JDialog;
 import javax.swing.JOptionPane;
-import javax.swing.UIManager;
 
 import org.openstreetmap.josm.Main;
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ComparePairListCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ComparePairListCellRenderer.java	(revision 1953)
+++ 	(revision )
@@ -1,30 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-
-import java.awt.Color;
-import java.awt.Component;
-
-import javax.swing.JLabel;
-import javax.swing.JList;
-import javax.swing.ListCellRenderer;
-
-public class ComparePairListCellRenderer extends JLabel implements ListCellRenderer {
-    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
-
-    public ComparePairListCellRenderer() {
-        setOpaque(true);
-    }
-    public Component getListCellRendererComponent(
-            JList list,
-            Object value,
-            int index,
-            boolean isSelected,
-            boolean cellHasFocus)
-    {
-        ComparePairType type = (ComparePairType)value;
-        setText(type.getDisplayName());
-        setBackground(isSelected ? BGCOLOR_SELECTED : Color.WHITE);
-        setForeground(Color.BLACK);
-        return this;
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ComparePairType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ComparePairType.java	(revision 1953)
+++ 	(revision )
@@ -1,88 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-import static org.openstreetmap.josm.gui.conflict.ListRole.MERGED_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.ListRole.MY_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.ListRole.THEIR_ENTRIES;
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-/**
- * Enumeration of the possible comparison pairs
- *
- */
-public enum ComparePairType {
-
-    /**
-     * compare my version of an {@see OsmPrimitive} with their version
-     */
-    MY_WITH_THEIR (tr("My with Their"), new ListRole[] {MY_ENTRIES, THEIR_ENTRIES}),
-
-    /**
-     * compare my version of an {@see OsmPrimitive} with the merged version
-     */
-    MY_WITH_MERGED (tr("My with Merged"),  new ListRole[] {MY_ENTRIES, MERGED_ENTRIES}),
-
-    /**
-     * compare their version of an {@see OsmPrimitive} with the merged veresion
-     */
-    THEIR_WITH_MERGED(tr("Their with Merged"),  new ListRole[] {THEIR_ENTRIES, MERGED_ENTRIES});
-
-    /** the localized display name */
-    private final String displayName;
-    private ListRole[] participatingRoles;
-
-    ComparePairType(String displayName, ListRole[] participatingRoles) {
-        this.displayName = displayName;
-        this.participatingRoles = participatingRoles;
-    }
-
-    /**
-     * replies the display name
-     * 
-     * @return the display name
-     */
-    public String getDisplayName() {
-        return displayName;
-    }
-
-    /**
-     * replies true, if <code>role</code> is participating in this comparison
-     * pair
-     * 
-     * @param role  the list role
-     * @return true, if <code>role</code> is participating in this comparison
-     * pair; false, otherwise
-     */
-    public boolean isParticipatingIn(ListRole role) {
-        for (ListRole r: participatingRoles) {
-            if (r.equals(role)) return true;
-        }
-        return false;
-    }
-
-    /**
-     * replies the pair of {@see ListRole}s participating in this comparison
-     * pair
-     * 
-     * @return  the pair of list roles
-     */
-    public ListRole[] getParticipatingRoles() {
-        return participatingRoles;
-    }
-
-    /**
-     * replies the opposite role of <code>role</code> participating in this comparison
-     * pair
-     * 
-     * @param role one of the two roles in this pair
-     * @return the opposite role
-     * @exception IllegalStateException  if role is not participating in this pair
-     */
-    public ListRole getOppositeRole(ListRole role) {
-        if (!isParticipatingIn(role))
-            throw new IllegalStateException(tr("role {0} is not participating in compare pair {1}", role.toString(), this.toString()));
-        if (participatingRoles[0].equals(role))
-            return participatingRoles[1];
-        else
-            return participatingRoles[0];
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ConflictResolver.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ConflictResolver.java	(revision 1953)
+++ 	(revision )
@@ -1,327 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.BorderLayout;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
-import java.util.logging.Logger;
-
-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.VersionConflictResolveCommand;
-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.nodes.NodeListMergeModel;
-import org.openstreetmap.josm.gui.conflict.nodes.NodeListMerger;
-import org.openstreetmap.josm.gui.conflict.properties.OperationCancelledException;
-import org.openstreetmap.josm.gui.conflict.properties.PropertiesMergeModel;
-import org.openstreetmap.josm.gui.conflict.properties.PropertiesMerger;
-import org.openstreetmap.josm.gui.conflict.relation.RelationMemberListMergeModel;
-import org.openstreetmap.josm.gui.conflict.relation.RelationMemberMerger;
-import org.openstreetmap.josm.gui.conflict.tags.TagMergeModel;
-import org.openstreetmap.josm.gui.conflict.tags.TagMerger;
-import org.openstreetmap.josm.tools.ImageProvider;
-
-/**
- * An UI component for resolving conflicts between two {@see OsmPrimitive}s.
- * 
- * This component emits {@see PropertyChangeEvent}s for three properties:
- * <ul>
- *   <li>{@see #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is
- *   completely resolved</li>
- *   <li>{@see #MY_PRIMITIVE_PROP} - new value is the {@see OsmPrimitive} in the role of
- *   my primitive</li>
- *   <li>{@see #THEIR_PRIMITIVE_PROP} - new value is the {@see OsmPrimitive} in the role of
- *   their primitive</li>
- * </ul>
- * 
- */
-public class ConflictResolver extends JPanel implements PropertyChangeListener  {
-
-    /* -------------------------------------------------------------------------------------- */
-    /* Property names                                                                         */
-    /* -------------------------------------------------------------------------------------- */
-    /** name of the property indicating whether all conflicts are resolved,
-     *  {@see #isResolvedCompletely()}
-     */
-    static public final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely";
-    /**
-     * name of the property for the {@see OsmPrimitive} in the role "my"
-     */
-    static public final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive";
-
-    /**
-     * name of the property for the {@see OsmPrimitive} in the role "my"
-     */
-    static public final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive";
-
-
-    private static final Logger logger = Logger.getLogger(ConflictResolver.class.getName());
-
-    private JTabbedPane tabbedPane = null;
-    private TagMerger tagMerger;
-    private NodeListMerger nodeListMerger;
-    private RelationMemberMerger relationMemberMerger;
-    private PropertiesMerger propertiesMerger;
-    private OsmPrimitive my;
-    private OsmPrimitive their;
-
-    private ImageIcon mergeComplete;
-    private ImageIcon mergeIncomplete;
-
-    /** indicates whether the current conflict is resolved completely */
-    private boolean resolvedCompletely;
-
-    /**
-     * loads the required icons
-     */
-    protected void loadIcons() {
-        mergeComplete = ImageProvider.get("dialogs/conflict","mergecomplete.png" );
-        mergeIncomplete = ImageProvider.get("dialogs/conflict","mergeincomplete.png" );
-    }
-
-    /**
-     * builds the UI
-     */
-    protected 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);
-    }
-
-    /**
-     * constructor
-     */
-    public ConflictResolver() {
-        resolvedCompletely = false;
-        build();
-        loadIcons();
-    }
-
-    /**
-     * Sets the {@see 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 {@see 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
-     */
-    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, tr("Tags({0} conflicts)", newValue));
-                tabbedPane.setToolTipTextAt(1, tr("{0} pending tag conflicts to be resolved"));
-                tabbedPane.setIconAt(1, mergeIncomplete);
-            }
-            updateResolvedCompletely();
-        } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
-            boolean frozen = (Boolean)evt.getNewValue();
-            if (frozen && evt.getSource() == nodeListMerger.getModel()) {
-                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);
-            }
-            if (frozen && evt.getSource() == relationMemberMerger.getModel()) {
-                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();
-        }
-    }
-
-    /**
-     * populates the conflict resolver with the conflicts between my and their
-     * 
-     * @param my   my primitive (i.e. the primitive in the local dataset)
-     * @param their their primitive (i.e. the primitive in the server dataset)
-     * 
-     */
-    public void populate(OsmPrimitive my, OsmPrimitive their) {
-        setMy(my);
-        setTheir(their);
-        propertiesMerger.getModel().populate(my, their);
-        if (propertiesMerger.getModel().hasVisibleStateConflict()) {
-            tabbedPane.setEnabledAt(1, false);
-            tabbedPane.setEnabledAt(2, false);
-            tabbedPane.setEnabledAt(3, false);
-            return;
-        }
-        tabbedPane.setEnabledAt(0, true);
-        tagMerger.getModel().populate(my, their);
-        tabbedPane.setEnabledAt(1, true);
-
-        if (my instanceof Node) {
-            tabbedPane.setEnabledAt(2,false);
-            tabbedPane.setEnabledAt(3,false);
-        } else if (my instanceof Way) {
-            nodeListMerger.populate((Way)my, (Way)their);
-            tabbedPane.setEnabledAt(2, true);
-            tabbedPane.setEnabledAt(3, false);
-            tabbedPane.setTitleAt(3,tr("Members"));
-            tabbedPane.setIconAt(3, null);
-        } else if (my instanceof Relation) {
-            relationMemberMerger.populate((Relation)my, (Relation)their);
-            tabbedPane.setEnabledAt(2, false);
-            tabbedPane.setTitleAt(2,tr("Nodes"));
-            tabbedPane.setIconAt(2, null);
-            tabbedPane.setEnabledAt(3, true);
-        }
-        updateResolvedCompletely();
-    }
-
-    /**
-     * Builds the resolution command(s) for the resolved conflicts in this
-     * ConflictResolver
-     * 
-     * @return the resolution command
-     */
-    public Command buildResolveCommand() throws OperationCancelledException {
-        ArrayList<Command> commands = new ArrayList<Command>();
-        if (propertiesMerger.getModel().hasVisibleStateConflict()) {
-            if (propertiesMerger.getModel().isDecidedVisibleState()) {
-                commands.addAll(propertiesMerger.getModel().buildResolveCommand(my, their));
-            }
-        } else {
-            if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
-                commands.add(tagMerger.getModel().buildResolveCommand(my, their));
-            }
-            commands.addAll(propertiesMerger.getModel().buildResolveCommand(my, their));
-            if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
-                NodeListMergeModel model  =(NodeListMergeModel)nodeListMerger.getModel();
-                commands.add(model.buildResolveCommand((Way)my, (Way)their));
-            } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
-                RelationMemberListMergeModel model  =(RelationMemberListMergeModel)relationMemberMerger.getModel();
-                commands.add(model.buildResolveCommand((Relation)my, (Relation)their));
-            }
-            if (isResolvedCompletely()) {
-                commands.add(
-                        new VersionConflictResolveCommand(my, their)
-                );
-            }
-        }
-        return new SequenceCommand(tr("Conflict Resolution"), commands);
-    }
-
-    /**
-     * Updates the state of the property {@see #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;
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ListMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ListMergeModel.java	(revision 1953)
+++ 	(revision )
@@ -1,787 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-
-import static org.openstreetmap.josm.gui.conflict.ComparePairType.MY_WITH_MERGED;
-import static org.openstreetmap.josm.gui.conflict.ComparePairType.MY_WITH_THEIR;
-import static org.openstreetmap.josm.gui.conflict.ComparePairType.THEIR_WITH_MERGED;
-import static org.openstreetmap.josm.gui.conflict.ListRole.MERGED_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.ListRole.MY_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.ListRole.THEIR_ENTRIES;
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Observable;
-import java.util.logging.Logger;
-
-import javax.swing.AbstractListModel;
-import javax.swing.ComboBoxModel;
-import javax.swing.DefaultListSelectionModel;
-import javax.swing.table.DefaultTableModel;
-import javax.swing.table.TableModel;
-
-/**
- * ListMergeModel is a model for interactively comparing and merging two list of entries
- * of type T. It maintains three lists of entries of type T:
- * <ol>
- *   <li>the list of <em>my</em> entries</li>
- *   <li>the list of <em>their</em> entries</li>
- *   <li>the list of <em>merged</em> entries</li>
- * </ol>
- * 
- * A ListMergeModel is a factory for three {@see TableModel}s and three {@see ListSelectionModel}s:
- * <ol>
- *   <li>the table model and the list selection for for a  {@see JTable} which shows my entries.
- *    See {@see #getMyTableModel()}</li> and {@see ListMergeModel#getMySelectionModel()}</li>
- *   <li>dito for their entries and merged entries</li>
- * </ol>
- *
- * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
- * decisions. {@see PropertyChangeListener}s can register for property value changes of
- * {@see #PROP_FROZEN}.
- * 
- * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
- * <ul>
- *   <li>{@see ListMergeModel#cloneEntryForMergedList(Object)} - clones an entry of type T</li>
- *   <li>{@see ListMergeModel#isEqualEntry(Object, Object)} - checks whether two entries are equals </li>
- *   <li>{@see ListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
- *     a JTable, dispatched from {@see TableModel#setValueAt(Object, int, int)} </li>
- * </ul>
- * A ListMergeModel is used in combination with a {@see ListMerger}.
- *
- * @param <T>  the type of the list entries
- * @see ListMerger
- */
-public abstract class ListMergeModel<T> extends Observable {
-    private static final Logger logger = Logger.getLogger(ListMergeModel.class.getName());
-
-    public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen";
-
-    protected HashMap<ListRole, ArrayList<T>> entries;
-
-    protected DefaultTableModel myEntriesTableModel;
-    protected DefaultTableModel theirEntriesTableModel;
-    protected DefaultTableModel mergedEntriesTableModel;
-
-    protected EntriesSelectionModel myEntriesSelectionModel;
-    protected EntriesSelectionModel theirEntriesSelectionModel;
-    protected EntriesSelectionModel mergedEntriesSelectionModel;
-
-    private final ArrayList<PropertyChangeListener> listeners;
-    private boolean isFrozen = false;
-    private final ComparePairListModel comparePairListModel;
-
-
-
-    /**
-     * Creates a clone of an entry of type T suitable to be included in the
-     * list of merged entries
-     * 
-     * @param entry the entry
-     * @return the cloned entry
-     */
-    protected abstract T cloneEntryForMergedList(T entry);
-
-    /**
-     * checks whether two entries are equal. This is not necessarily the same as
-     * e1.equals(e2).
-     * 
-     * @param e1  the first entry
-     * @param e2  the second entry
-     * @return true, if the entries are equal, false otherwise.
-     */
-    public abstract boolean isEqualEntry(T e1, T e2);
-
-    /**
-     * Handles method dispatches from {@see TableModel#setValueAt(Object, int, int)}.
-     * 
-     * @param model the table model
-     * @param value  the value to be set
-     * @param row  the row index
-     * @param col the column index
-     * 
-     * @see TableModel#setValueAt(Object, int, int)
-     */
-    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
-
-
-
-    protected void buildMyEntriesTableModel() {
-        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
-    }
-
-    protected void buildTheirEntriesTableModel() {
-        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
-    }
-
-    protected void buildMergedEntriesTableModel() {
-        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
-    }
-
-    protected ArrayList<T> getMergedEntries() {
-        return entries.get(MERGED_ENTRIES);
-    }
-    protected ArrayList<T> getMyEntries() {
-        return entries.get(MY_ENTRIES);
-    }
-    protected ArrayList<T> getTheirEntries() {
-        return entries.get(THEIR_ENTRIES);
-    }
-
-    public int getMyEntriesSize() {
-        return getMyEntries().size();
-    }
-
-    public int getMergedEntriesSize() {
-        return getMergedEntries().size();
-    }
-
-    public int getTheirEntriesSize() {
-        return getTheirEntries().size();
-    }
-
-    public ListMergeModel() {
-        entries = new HashMap<ListRole, ArrayList<T>>();
-        for (ListRole role : ListRole.values()) {
-            entries.put(role, new ArrayList<T>());
-        }
-
-        buildMyEntriesTableModel();
-        buildTheirEntriesTableModel();
-        buildMergedEntriesTableModel();
-
-        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
-        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
-        mergedEntriesSelectionModel =  new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
-
-        listeners = new ArrayList<PropertyChangeListener>();
-        comparePairListModel = new ComparePairListModel();
-
-        setFrozen(true);
-    }
-
-    public void addPropertyChangeListener(PropertyChangeListener listener) {
-        synchronized(listeners) {
-            if (listener != null && ! listeners.contains(listener)) {
-                listeners.add(listener);
-            }
-        }
-    }
-
-    public void removePropertyChangeListener(PropertyChangeListener listener) {
-        synchronized(listeners) {
-            if (listener != null && listeners.contains(listener)) {
-                listeners.remove(listener);
-            }
-        }
-    }
-
-    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
-        synchronized(listeners) {
-            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
-            for (PropertyChangeListener listener: listeners) {
-                listener.propertyChange(evt);
-            }
-        }
-    }
-
-    public void setFrozen(boolean isFrozen) {
-        boolean oldValue = this.isFrozen;
-        this.isFrozen = isFrozen;
-        fireFrozenChanged(oldValue, this.isFrozen);
-    }
-
-    public boolean isFrozen() {
-        return isFrozen;
-    }
-
-    public TableModel getMyTableModel() {
-        return myEntriesTableModel;
-    }
-
-    public TableModel getTheirTableModel() {
-        return theirEntriesTableModel;
-    }
-
-    public TableModel getMergedTableModel() {
-        return mergedEntriesTableModel;
-    }
-
-    public EntriesSelectionModel getMySelectionModel() {
-        return myEntriesSelectionModel;
-    }
-
-    public EntriesSelectionModel getTheirSelectionModel() {
-        return theirEntriesSelectionModel;
-    }
-
-    public EntriesSelectionModel getMergedSelectionModel() {
-        return mergedEntriesSelectionModel;
-    }
-
-    protected void fireModelDataChanged() {
-        myEntriesTableModel.fireTableDataChanged();
-        theirEntriesTableModel.fireTableDataChanged();
-        mergedEntriesTableModel.fireTableDataChanged();
-        setChanged();
-        notifyObservers();
-    }
-
-    protected void copyToTop(ListRole role, int []rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        for (int i = rows.length - 1; i >= 0; i--) {
-            int row = rows[i];
-            T n = entries.get(role).get(row);
-            entries.get(MERGED_ENTRIES).add(0, cloneEntryForMergedList(n));
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes at the top of the list of merged
-     * nodes.
-     * 
-     * @param rows the indices
-     */
-    public void copyMyToTop(int [] rows) {
-        copyToTop(MY_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes at the top of the list of merged
-     * nodes.
-     * 
-     * @param rows the indices
-     */
-    public void copyTheirToTop(int [] rows) {
-        copyToTop(THEIR_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes in source to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     * 
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     */
-
-    public void copyToEnd(ListRole source, int [] rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        ArrayList<T> mergedEntries = getMergedEntries();
-        for (int row : rows) {
-            T n = entries.get(source).get(row);
-            mergedEntries.add(cloneEntryForMergedList(n));
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.setSelectionInterval(mergedEntries.size()-rows.length, mergedEntries.size() -1);
-
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     * 
-     * @param rows the indices
-     */
-    public void copyMyToEnd(int [] rows) {
-        copyToEnd(MY_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     * 
-     * @param rows the indices
-     */
-    public void copyTheirToEnd(int [] rows) {
-        copyToEnd(THEIR_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     * 
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    protected void copyBeforeCurrent(ListRole source, int [] rows, int current) {
-        if (rows == null || rows.length == 0)
-            return;
-        ArrayList<T> mergedEntries = getMergedEntries();
-        if (current < 0 || current >= mergedEntries.size())
-            throw new IllegalArgumentException(tr("parameter current out of range: got {0}", current));
-        for (int i=rows.length -1; i>=0; i--) {
-            int row = rows[i];
-            T n = entries.get(source).get(row);
-            mergedEntries.add(current, cloneEntryForMergedList(n));
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     * 
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    public void copyMyBeforeCurrent(int [] rows, int current) {
-        copyBeforeCurrent(MY_ENTRIES,rows,current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     * 
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    public void copyTheirBeforeCurrent(int [] rows, int current) {
-        copyBeforeCurrent(THEIR_ENTRIES,rows,current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     * 
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    protected void copyAfterCurrent(ListRole source, int [] rows, int current) {
-        if (rows == null || rows.length == 0)
-            return;
-        ArrayList<T> mergedEntries = getMergedEntries();
-
-        if (current < 0 || current >= mergedEntries.size())
-            throw new IllegalArgumentException(tr("parameter current out of range: got {0}", current));
-        if (current == mergedEntries.size() -1) {
-            copyToEnd(source, rows);
-        } else {
-            for (int i=rows.length -1; i>=0; i--) {
-                int row = rows[i];
-                T n = entries.get(source).get(row);
-                mergedEntries.add(current+1, cloneEntryForMergedList(n));
-            }
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
-        notifyObservers();
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     * 
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    public void copyMyAfterCurrent(int [] rows, int current) {
-        copyAfterCurrent(MY_ENTRIES, rows, current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     * 
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
-     * 
-     */
-    public void copyTheirAfterCurrent(int [] rows, int current) {
-        copyAfterCurrent(THEIR_ENTRIES, rows, current);
-    }
-
-    /**
-     * Moves the nodes given by indices in rows  up by one position in the list
-     * of merged nodes.
-     * 
-     * @param rows the indices
-     * 
-     */
-    public void moveUpMerged(int [] rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        if (rows[0] == 0)
-            // can't move up
-            return;
-        ArrayList<T> mergedEntries = getMergedEntries();
-        for (int row: rows) {
-            T n = mergedEntries.get(row);
-            mergedEntries.remove(row);
-            mergedEntries.add(row -1, n);
-        }
-        fireModelDataChanged();
-        notifyObservers();
-        mergedEntriesSelectionModel.clearSelection();
-        for (int row: rows) {
-            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
-        }
-    }
-
-    /**
-     * Moves the nodes given by indices in rows down by one position in the list
-     * of merged nodes.
-     * 
-     * @param rows the indices
-     */
-    public void moveDownMerged(int [] rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        ArrayList<T> mergedEntries = getMergedEntries();
-        if (rows[rows.length -1] == mergedEntries.size() -1)
-            // can't move down
-            return;
-        for (int i = rows.length-1; i>=0;i--) {
-            int row = rows[i];
-            T n = mergedEntries.get(row);
-            mergedEntries.remove(row);
-            mergedEntries.add(row +1, n);
-        }
-        fireModelDataChanged();
-        notifyObservers();
-        mergedEntriesSelectionModel.clearSelection();
-        for (int row: rows) {
-            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
-        }
-    }
-
-    /**
-     * Removes the nodes given by indices in rows from the list
-     * of merged nodes.
-     * 
-     * @param rows the indices
-     */
-    public void removeMerged(int [] rows) {
-        if (rows == null || rows.length == 0)
-            return;
-
-        ArrayList<T> mergedEntries = getMergedEntries();
-
-        for (int i = rows.length-1; i>=0;i--) {
-            mergedEntries.remove(rows[i]);
-        }
-        fireModelDataChanged();
-        notifyObservers();
-        mergedEntriesSelectionModel.clearSelection();
-    }
-
-
-    /**
-     * Replies true if the list of my entries and the list of their
-     * entries are equal
-     * 
-     * @return true, if the lists are equal; false otherwise
-     */
-    protected boolean myAndTheirEntriesEqual() {
-
-        if (getMyEntries().size() != getTheirEntries().size())
-            return false;
-        for (int i=0; i < getMyEntries().size(); i++) {
-            if (! isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
-                return false;
-        }
-        return true;
-    }
-
-
-    /**
-     * This an adapter between a {@see JTable} and one of the three entry lists
-     * in the role {@see ListRole} managed by the {@see ListMergeModel}.
-     * 
-     * From the point of view of the {@see JTable} it is a {@see TableModel}.
-     *
-     * @param <T>
-     * @see ListMergeModel#getMyTableModel()
-     * @see ListMergeModel#getTheirTableModel()
-     * @see ListMergeModel#getMergedTableModel()
-     */
-    public class EntriesTableModel extends DefaultTableModel {
-        private final ListRole role;
-
-        /**
-         * 
-         * @param role the role
-         */
-        public EntriesTableModel(ListRole role) {
-            this.role = role;
-        }
-
-        @Override
-        public int getRowCount() {
-            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
-            count = Math.max(count, getTheirEntries().size());
-            return count;
-        }
-
-        @Override
-        public Object getValueAt(int row, int column) {
-            if (row < entries.get(role).size())
-                return entries.get(role).get(row);
-            return null;
-        }
-
-        @Override
-        public boolean isCellEditable(int row, int column) {
-            return false;
-        }
-
-        @Override
-        public void setValueAt(Object value, int row, int col) {
-            ListMergeModel.this.setValueAt(this, value,row,col);
-        }
-
-        public ListMergeModel getListMergeModel() {
-            return ListMergeModel.this;
-        }
-
-        /**
-         * replies true if the {@see ListRole} of this {@see EntriesTableModel}
-         * participates in the current {@see ComparePairType}
-         * 
-         * @return true, if the if the {@see ListRole} of this {@see EntriesTableModel}
-         * participates in the current {@see ComparePairType}
-         * 
-         * @see ComparePairListModel#getSelectedComparePair()
-         */
-        public boolean isParticipatingInCurrentComparePair() {
-            return getComparePairListModel()
-            .getSelectedComparePair()
-            .isParticipatingIn(role);
-        }
-
-        /**
-         * replies true if the entry at <code>row</code> is equal to the entry at the
-         * same position in the opposite list of the current {@see ComparePairType}.
-         * 
-         * @param row  the row number
-         * @return true if the entry at <code>row</code> is equal to the entry at the
-         * same position in the opposite list of the current {@see ComparePairType}
-         * @exception IllegalStateException thrown, if this model is not participating in the
-         *   current  {@see ComparePairType}
-         * @see ComparePairType#getOppositeRole(ListRole)
-         * @see #getRole()
-         * @see #getOppositeEntries()
-         */
-        public boolean isSamePositionInOppositeList(int row) {
-            if (!isParticipatingInCurrentComparePair())
-                throw new IllegalStateException(tr("list in role {0} is currently not participating in a compare pair", role.toString()));
-            if (row >= getEntries().size()) return false;
-            if (row >= getOppositeEntries().size()) return false;
-
-            T e1 = getEntries().get(row);
-            T e2 = getOppositeEntries().get(row);
-            return isEqualEntry(e1, e2);
-        }
-
-        /**
-         * replies true if the entry at the current position is present in the opposite list
-         * of the current {@see ComparePairType}.
-         * 
-         * @param row the current row
-         * @return true if the entry at the current position is present in the opposite list
-         * of the current {@see ComparePairType}.
-         * @exception IllegalStateException thrown, if this model is not participating in the
-         *   current  {@see ComparePairType}
-         * @see ComparePairType#getOppositeRole(ListRole)
-         * @see #getRole()
-         * @see #getOppositeEntries()
-         */
-        public boolean isIncludedInOppositeList(int row) {
-            if (!isParticipatingInCurrentComparePair())
-                throw new IllegalStateException(tr("list in role {0} is currently not participating in a compare pair", role.toString()));
-
-            if (row >= getEntries().size()) return false;
-            T e1 = getEntries().get(row);
-            for (T e2: getOppositeEntries()) {
-                if (isEqualEntry(e1, e2)) return true;
-            }
-            return false;
-        }
-
-        protected ArrayList<T> getEntries() {
-            return entries.get(role);
-        }
-
-        /**
-         * replies the opposite list of entries with respect to the current {@see ComparePairType}
-         * 
-         * @return the opposite list of entries
-         */
-        protected ArrayList<T> getOppositeEntries() {
-            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
-            return entries.get(opposite);
-        }
-
-        public ListRole getRole() {
-            return role;
-        }
-    }
-
-    /**
-     * This is the selection model to be used in a {@see JTable} which displays
-     * an entry list managed by {@see ListMergeModel}.
-     * 
-     * The model ensures that only rows displaying an entry in the entry list
-     * can be selected. "Empty" rows can't be selected.
-     * 
-     * @see ListMergeModel#getMySelectionModel()
-     * @see ListMergeModel#getMergedSelectionModel()
-     * @see ListMergeModel#getTheirSelectionModel()
-     *
-     */
-    protected class EntriesSelectionModel extends DefaultListSelectionModel {
-        private final ArrayList<T> entries;
-
-        public EntriesSelectionModel(ArrayList<T> nodes) {
-            this.entries = nodes;
-        }
-
-        @Override
-        public void addSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            if (index0 > entries.size() - 1) return;
-            index0 = Math.min(entries.size()-1, index0);
-            index1 = Math.min(entries.size()-1, index1);
-            super.addSelectionInterval(index0, index1);
-        }
-
-        @Override
-        public void insertIndexInterval(int index, int length, boolean before) {
-            if (entries.isEmpty()) return;
-            if (before) {
-                int newindex = Math.min(entries.size()-1, index);
-                if (newindex < index - length) return;
-                length = length - (index - newindex);
-                super.insertIndexInterval(newindex, length, before);
-            } else {
-                if (index > entries.size() -1) return;
-                length = Math.min(entries.size()-1 - index, length);
-                super.insertIndexInterval(index, length, before);
-            }
-        }
-
-        @Override
-        public void moveLeadSelectionIndex(int leadIndex) {
-            if (entries.isEmpty()) return;
-            leadIndex = Math.max(0, leadIndex);
-            leadIndex = Math.min(entries.size() - 1, leadIndex);
-            super.moveLeadSelectionIndex(leadIndex);
-        }
-
-        @Override
-        public void removeIndexInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-            super.removeIndexInterval(index0, index1);
-        }
-
-        @Override
-        public void removeSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-            super.removeSelectionInterval(index0, index1);
-        }
-
-        @Override
-        public void setAnchorSelectionIndex(int anchorIndex) {
-            if (entries.isEmpty()) return;
-            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
-            super.setAnchorSelectionIndex(anchorIndex);
-        }
-
-        @Override
-        public void setLeadSelectionIndex(int leadIndex) {
-            if (entries.isEmpty()) return;
-            leadIndex = Math.min(entries.size() - 1, leadIndex);
-            super.setLeadSelectionIndex(leadIndex);
-        }
-
-        @Override
-        public void setSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-
-            super.setSelectionInterval(index0, index1);
-        }
-    }
-
-    public ComparePairListModel getComparePairListModel() {
-        return this.comparePairListModel;
-    }
-
-    public class ComparePairListModel extends AbstractListModel implements ComboBoxModel {
-
-        private  int selectedIdx;
-        private final ArrayList<ComparePairType> compareModes;
-
-        public ComparePairListModel() {
-            this.compareModes = new ArrayList<ComparePairType>();
-            compareModes.add(MY_WITH_THEIR);
-            compareModes.add(MY_WITH_MERGED);
-            compareModes.add(THEIR_WITH_MERGED);
-            selectedIdx = 0;
-        }
-
-        public Object getElementAt(int index) {
-            if (index < compareModes.size())
-                return compareModes.get(index);
-            throw new IllegalArgumentException(tr("unexpected value of parameter \"index\". Got {0}", index));
-        }
-
-        public int getSize() {
-            return compareModes.size();
-        }
-
-        public Object getSelectedItem() {
-            return compareModes.get(selectedIdx);
-        }
-
-        public void setSelectedItem(Object anItem) {
-            int i = compareModes.indexOf(anItem);
-            if (i < 0)
-                throw new IllegalStateException(tr("item {0} not found in list", anItem));
-            selectedIdx = i;
-            fireModelDataChanged();
-        }
-
-        public ComparePairType getSelectedComparePair() {
-            return compareModes.get(selectedIdx);
-        }
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ListMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ListMerger.java	(revision 1953)
+++ 	(revision )
@@ -1,964 +1,0 @@
-package org.openstreetmap.josm.gui.conflict;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
-
-import java.awt.Adjustable;
-import java.awt.FlowLayout;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.AdjustmentEvent;
-import java.awt.event.AdjustmentListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Observable;
-import java.util.Observer;
-import java.util.logging.Logger;
-
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.ImageIcon;
-import javax.swing.JButton;
-import javax.swing.JCheckBox;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JTable;
-import javax.swing.JToggleButton;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-
-import org.openstreetmap.josm.tools.ImageProvider;
-
-/**
- * A UI component for resolving conflicts in two lists of entries of type T.
- *
- * @param T  the type of the entries
- * @see ListMergeModel
- */
-public abstract class ListMerger<T> extends JPanel implements PropertyChangeListener, Observer {
-    private static final Logger logger = Logger.getLogger(ListMerger.class.getName());
-
-    protected JTable myEntriesTable;
-    protected JTable mergedEntriesTable;
-    protected JTable theirEntriesTable;
-
-    protected ListMergeModel<T> model;
-
-    private CopyStartLeftAction copyStartLeftAction;
-    private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
-    private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
-    private CopyEndLeftAction copyEndLeftAction;
-
-    private CopyStartRightAction copyStartRightAction;
-    private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
-    private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
-    private CopyEndRightAction copyEndRightAction;
-
-    private MoveUpMergedAction moveUpMergedAction;
-    private MoveDownMergedAction moveDownMergedAction;
-    private RemoveMergedAction removeMergedAction;
-    private FreezeAction freezeAction;
-
-    private AdjustmentSynchronizer adjustmentSynchronizer;
-
-    private  JCheckBox cbLockMyScrolling;
-    private  JCheckBox cbLockMergedScrolling;
-    private  JCheckBox cbLockTheirScrolling;
-
-    private  JLabel lblMyVersion;
-    private  JLabel lblMergedVersion;
-    private  JLabel lblTheirVersion;
-
-
-    private  JLabel lblFrozenState;
-
-    abstract protected JScrollPane buildMyElementsTable();
-    abstract protected JScrollPane buildMergedElementsTable();
-    abstract protected JScrollPane buildTheirElementsTable();
-
-    protected JScrollPane embeddInScrollPane(JTable table) {
-        JScrollPane pane = new JScrollPane(table);
-        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
-        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
-        if (adjustmentSynchronizer == null) {
-            adjustmentSynchronizer = new AdjustmentSynchronizer();
-        }
-        return pane;
-    }
-
-    protected void wireActionsToSelectionModels() {
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
-
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
-
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
-    }
-
-    protected JPanel buildLeftButtonPanel() {
-        JPanel pnl = new JPanel();
-        pnl.setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        copyStartLeftAction = new CopyStartLeftAction();
-        JButton btn = new JButton(copyStartLeftAction);
-        btn.setName("button.copystartleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 1;
-        copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
-        btn = new JButton(copyBeforeCurrentLeftAction);
-        btn.setName("button.copybeforecurrentleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 2;
-        copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
-        btn = new JButton(copyAfterCurrentLeftAction);
-        btn.setName("button.copyaftercurrentleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 3;
-        copyEndLeftAction = new CopyEndLeftAction();
-        btn = new JButton(copyEndLeftAction);
-        btn.setName("button.copyendleft");
-        pnl.add(btn, gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildRightButtonPanel() {
-        JPanel pnl = new JPanel();
-        pnl.setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        copyStartRightAction = new CopyStartRightAction();
-        pnl.add(new JButton(copyStartRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 1;
-        copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
-        pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 2;
-        copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
-        pnl.add(new JButton(copyAfterCurrentRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 3;
-        copyEndRightAction = new CopyEndRightAction();
-        pnl.add(new JButton(copyEndRightAction), gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildMergedListControlButtons() {
-        JPanel pnl = new JPanel();
-        pnl.setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.3;
-        gc.weighty = 0.0;
-        moveUpMergedAction = new MoveUpMergedAction();
-        pnl.add(new JButton(moveUpMergedAction), gc);
-
-        gc.gridx = 1;
-        gc.gridy = 0;
-        moveDownMergedAction = new MoveDownMergedAction();
-        pnl.add(new JButton(moveDownMergedAction), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 0;
-        removeMergedAction = new RemoveMergedAction();
-        pnl.add(new JButton(removeMergedAction), gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
-        JPanel panel = new JPanel();
-        panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
-        panel.add(new JLabel(tr("lock scrolling")));
-        panel.add(cb);
-        return panel;
-    }
-
-    protected JPanel buildComparePairSelectionPanel() {
-        JPanel p = new JPanel();
-        p.setLayout(new FlowLayout(FlowLayout.LEFT));
-        p.add(new JLabel(tr("Compare ")));
-        JComboBox cbComparePair =new JComboBox(model.getComparePairListModel());
-        cbComparePair.setRenderer(new ComparePairListCellRenderer());
-        p.add(cbComparePair);
-        return p;
-    }
-
-    protected JPanel buildFrozeStateControlPanel() {
-        JPanel p = new JPanel();
-        p.setLayout(new FlowLayout(FlowLayout.LEFT));
-        lblFrozenState = new JLabel();
-        p.add(lblFrozenState);
-        freezeAction = new FreezeAction();
-        JToggleButton btn = new JToggleButton(freezeAction);
-        freezeAction.adapt(btn);
-        btn.setName("button.freeze");
-        p.add(btn);
-
-        return p;
-    }
-
-    protected void build() {
-        setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        // ------------------
-        gc.gridx = 0;
-        gc.gridy = 0;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        gc.insets = new Insets(10,0,0,0);
-        lblMyVersion = new JLabel(tr("My version"));
-        lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
-        add(lblMyVersion, gc);
-
-        gc.gridx = 2;
-        gc.gridy = 0;
-        lblMergedVersion = new JLabel(tr("Merged version"));
-        lblMergedVersion.setToolTipText(tr("List of merged elements. They will replace the my elements when the merge decisions are applied."));
-        add(lblMergedVersion, gc);
-
-        gc.gridx = 4;
-        gc.gridy = 0;
-        lblTheirVersion = new JLabel(tr("Their version"));
-        lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
-        add(lblTheirVersion, gc);
-
-        // ------------------------------
-        gc.gridx = 0;
-        gc.gridy = 1;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        gc.insets = new Insets(0,0,0,0);
-        cbLockMyScrolling = new JCheckBox();
-        cbLockMyScrolling.setName("checkbox.lockmyscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 1;
-        cbLockMergedScrolling = new JCheckBox();
-        cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
-
-        gc.gridx = 4;
-        gc.gridy = 1;
-        cbLockTheirScrolling = new JCheckBox();
-        cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
-
-        // --------------------------------
-        gc.gridx = 0;
-        gc.gridy = 2;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 1.0;
-        gc.insets = new Insets(0,0,0,0);
-        JScrollPane pane = buildMyElementsTable();
-        adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        gc.gridx = 1;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildLeftButtonPanel(), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        pane = buildMergedElementsTable();
-        adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        gc.gridx = 3;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildRightButtonPanel(), gc);
-
-        gc.gridx = 4;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        pane = buildTheirElementsTable();
-        adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        // ----------------------------------
-        gc.gridx = 2;
-        gc.gridy = 3;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildMergedListControlButtons(), gc);
-
-        // -----------------------------------
-        gc.gridx = 0;
-        gc.gridy = 4;
-        gc.gridwidth = 2;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.LINE_START;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildComparePairSelectionPanel(), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 4;
-        gc.gridwidth = 3;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.LINE_START;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildFrozeStateControlPanel(), gc);
-
-
-        wireActionsToSelectionModels();
-    }
-
-    public ListMerger(ListMergeModel<T> model) {
-        this.model = model;
-        model.addObserver(this);
-        build();
-        model.addPropertyChangeListener(this);
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
-     *
-     */
-    class CopyStartLeftAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyStartLeftAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartleft.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("> top"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes to the start of the merged node list"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = myEntriesTable.getSelectedRows();
-            model.copyMyToTop(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes at the end of the list of merged nodes.
-     *
-     */
-    class CopyEndLeftAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyEndLeftAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendleft.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("> bottom"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements to the end of the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = myEntriesTable.getSelectedRows();
-            model.copyMyToEnd(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
-     *
-     */
-    class CopyBeforeCurrentLeftAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyBeforeCurrentLeftAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentleft.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "> before");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements before the first selected element in the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] myRows = myEntriesTable.getSelectedRows();
-            int [] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int current = mergedRows[0];
-            model.copyMyBeforeCurrent(myRows, current);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
-     *
-     */
-    class CopyAfterCurrentLeftAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyAfterCurrentLeftAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentleft.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "> after");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements after the first selected element in the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] myRows = myEntriesTable.getSelectedRows();
-            int [] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int current = mergedRows[0];
-            model.copyMyAfterCurrent(myRows, current);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-
-    class CopyStartRightAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyStartRightAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartright.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "< top");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element to the start of the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = theirEntriesTable.getSelectedRows();
-            model.copyTheirToTop(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-
-    class CopyEndRightAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyEndRightAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendright.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "< bottom");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements to the end of the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = theirEntriesTable.getSelectedRows();
-            model.copyTheirToEnd(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    class CopyBeforeCurrentRightAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyBeforeCurrentRightAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentright.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "< before");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements before the first selected element in the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] myRows = theirEntriesTable.getSelectedRows();
-            int [] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int current = mergedRows[0];
-            model.copyTheirBeforeCurrent(myRows, current);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-
-    class CopyAfterCurrentRightAction extends AbstractAction implements ListSelectionListener {
-
-        public CopyAfterCurrentRightAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentright.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, "< after");
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element after the first selected element in the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] myRows = theirEntriesTable.getSelectedRows();
-            int [] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int current = mergedRows[0];
-            model.copyTheirAfterCurrent(myRows, current);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-
-    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
-
-        public MoveUpMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Up"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected elements by one position"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            model.moveUpMerged(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-                    && rows[0] != 0
-            );
-        }
-    }
-
-    /**
-     * Action for moving the currently selected entries in the list of merged entries
-     * one position down
-     *
-     */
-    class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
-
-        public MoveDownMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Down"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            model.moveDownMerged(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
-            );
-        }
-    }
-
-    /**
-     * Action for removing the selected entries in the list of merged entries
-     * from the list of merged entries.
-     *
-     */
-    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
-
-        public RemoveMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove.png");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Remove"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements"));
-            setEnabled(false);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            model.removeMerged(rows);
-        }
-
-        public void valueChanged(ListSelectionEvent e) {
-            int [] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-            );
-        }
-    }
-
-    static public interface FreezeActionProperties {
-        String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
-    }
-
-    /**
-     * Action for freezing the current state of the list merger
-     *
-     */
-    class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties  {
-
-        public FreezeAction() {
-            putValue(Action.NAME, tr("Freeze"));
-            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements"));
-            putValue(PROP_SELECTED, false);
-            setEnabled(true);
-        }
-
-        public void actionPerformed(ActionEvent arg0) {
-            // do nothing
-        }
-
-        /**
-         * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
-         * such that the action gets notified about item state changes and the button gets
-         * notified about selection state changes of the action.
-         *
-         * @param btn a toggle button
-         */
-        public void adapt(final JToggleButton btn) {
-            btn.addItemListener(this);
-            addPropertyChangeListener(
-                    new PropertyChangeListener() {
-                        public void propertyChange(PropertyChangeEvent evt) {
-                            if (evt.getPropertyName().equals(PROP_SELECTED)) {
-                                btn.setSelected((Boolean)evt.getNewValue());
-                            }
-                        }
-                    }
-            );
-        }
-
-        public void itemStateChanged(ItemEvent e) {
-            int state = e.getStateChange();
-            if (state == ItemEvent.SELECTED) {
-                putValue(Action.NAME, tr("Unfreeze"));
-                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging"));
-                model.setFrozen(true);
-            } else if (state == ItemEvent.DESELECTED) {
-                putValue(Action.NAME, tr("Freeze"));
-                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements"));
-                model.setFrozen(false);
-            }
-            boolean isSelected = (Boolean)getValue(PROP_SELECTED);
-            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
-                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
-            }
-
-        }
-    }
-
-    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
-        myEntriesTable.getSelectionModel().clearSelection();
-        myEntriesTable.setEnabled(!newValue);
-        theirEntriesTable.getSelectionModel().clearSelection();
-        theirEntriesTable.setEnabled(!newValue);
-        mergedEntriesTable.getSelectionModel().clearSelection();
-        mergedEntriesTable.setEnabled(!newValue);
-        if (freezeAction != null) {
-            freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
-        }
-        if (newValue) {
-            lblFrozenState.setText(
-                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries</html>",
-                            freezeAction.getValue(Action.NAME))
-            );
-        } else {
-            lblFrozenState.setText(
-                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries</html>",
-                            freezeAction.getValue(Action.NAME))
-            );
-        }
-    }
-
-    public void propertyChange(PropertyChangeEvent evt) {
-        if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
-            handlePropertyChangeFrozen((Boolean)evt.getOldValue(), (Boolean)evt.getNewValue());
-        }
-    }
-
-    public ListMergeModel<T> getModel() {
-        return model;
-    }
-
-
-    public void update(Observable o, Object arg) {
-        lblMyVersion.setText(
-                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
-        );
-        lblMergedVersion.setText(
-                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
-        );
-        lblTheirVersion.setText(
-                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
-        );
-    }
-
-    /**
-     * Synchronizes scrollbar adjustments between a set of
-     * {@see Adjustable}s. Whenever the adjustment of one of
-     * the registerd Adjustables is updated the adjustment of
-     * the other registered Adjustables is adjusted too.
-     *
-     */
-    class AdjustmentSynchronizer implements AdjustmentListener {
-
-        private final  ArrayList<Adjustable> synchronizedAdjustables;
-        private final  HashMap<Adjustable, Boolean> enabledMap;
-
-        private final Observable observable;
-
-        public AdjustmentSynchronizer() {
-            synchronizedAdjustables = new ArrayList<Adjustable>();
-            enabledMap = new HashMap<Adjustable, Boolean>();
-            observable = new Observable();
-        }
-
-
-        /**
-         * registers an {@see Adjustable} for participation in synchronized
-         * scrolling.
-         *
-         * @param adjustable the adjustable
-         */
-        public void participateInSynchronizedScrolling(Adjustable adjustable) {
-            if (adjustable == null)
-                return;
-            if (synchronizedAdjustables.contains(adjustable))
-                return;
-            synchronizedAdjustables.add(adjustable);
-            setParticipatingInSynchronizedScrolling(adjustable, true);
-            adjustable.addAdjustmentListener(this);
-        }
-
-        /**
-         * event handler for {@see AdjustmentEvent}s
-         *
-         */
-        public void adjustmentValueChanged(AdjustmentEvent e) {
-            if (! enabledMap.get(e.getAdjustable()))
-                return;
-            for (Adjustable a : synchronizedAdjustables) {
-                if (a != e.getAdjustable() && isParticipatingInSynchronizedScrolling(a)) {
-                    a.setValue(e.getValue());
-                }
-            }
-        }
-
-        /**
-         * sets whether adjustable participates in adjustment synchronization
-         * or not
-         *
-         * @param adjustable the adjustable
-         */
-        protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) {
-            if (adjustable == null)
-                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "adjustable"));
-
-            if (! synchronizedAdjustables.contains(adjustable))
-                throw new IllegalStateException(tr("adjustable {0} not registered yet. Can't set participation in synchronized adjustment",adjustable));
-
-            enabledMap.put(adjustable, isParticipating);
-            observable.notifyObservers();
-        }
-
-        /**
-         * returns true if an adjustable is participating in synchronized scrolling
-         *
-         * @param adjustable the adjustable
-         * @return true, if the adjustable is participating in synchronized scrolling, false otherwise
-         * @throws IllegalStateException thrown, if adjustable is not registered for synchronized scrolling
-         */
-        protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) throws IllegalStateException {
-            if (! synchronizedAdjustables.contains(adjustable))
-                throw new IllegalStateException(tr("adjustable {0} not registered yet",adjustable));
-
-            return enabledMap.get(adjustable);
-        }
-
-        /**
-         * wires a {@see JCheckBox} to  the adjustment synchronizer, in such a way  that:
-         * <li>
-         *   <ol>state changes in the checkbox control whether the adjustable participates
-         *      in synchronized adjustment</ol>
-         *   <ol>state changes in this {@see AdjustmentSynchronizer} are reflected in the
-         *      {@see JCheckBox}</ol>
-         * </li>
-         *
-         *
-         * @param view  the checkbox to control whether an adjustable participates in synchronized
-         *      adjustment
-         * @param adjustable the adjustable
-         * @exception IllegalArgumentException thrown, if view is null
-         * @exception IllegalArgumentException thrown, if adjustable is null
-         */
-        protected void adapt(final JCheckBox view, final Adjustable adjustable) throws IllegalArgumentException, IllegalStateException {
-            if (adjustable == null)
-                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "adjustable"));
-            if (view == null)
-                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "view"));
-
-            if (! synchronizedAdjustables.contains(adjustable)) {
-                participateInSynchronizedScrolling(adjustable);
-            }
-
-            // register an item lister with the check box
-            //
-            view.addItemListener(new ItemListener() {
-                public void itemStateChanged(ItemEvent e) {
-                    switch(e.getStateChange()) {
-                    case ItemEvent.SELECTED:
-                        if (!isParticipatingInSynchronizedScrolling(adjustable)) {
-                            setParticipatingInSynchronizedScrolling(adjustable, true);
-                        }
-                        break;
-                    case ItemEvent.DESELECTED:
-                        if (isParticipatingInSynchronizedScrolling(adjustable)) {
-                            setParticipatingInSynchronizedScrolling(adjustable, false);
-                        }
-                        break;
-                    }
-                }
-            });
-
-
-            observable.addObserver(
-                    new Observer() {
-                        public void update(Observable o, Object arg) {
-                            boolean sync = isParticipatingInSynchronizedScrolling(adjustable);
-                            if (view.isSelected() != sync) {
-                                view.setSelected(sync);
-                            }
-                        }
-                    }
-            );
-            setParticipatingInSynchronizedScrolling(adjustable, true);
-            view.setSelected(true);
-        }
-    }
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ListRole.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ListRole.java	(revision 1953)
+++ 	(revision )
@@ -1,11 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-
-/**
- * Enumeration of roles entry lists play in {@see ListMergeModel}
- */
-public enum ListRole {
-    MY_ENTRIES,
-    MERGED_ENTRIES,
-    THEIR_ENTRIES
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/MergeDecisionType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/MergeDecisionType.java	(revision 1953)
+++ 	(revision )
@@ -1,8 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict;
-
-public enum MergeDecisionType {
-    KEEP_MINE,
-    KEEP_THEIR,
-    UNDECIDED,
-}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairListCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairListCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairListCellRenderer.java	(revision 1954)
@@ -0,0 +1,30 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+
+public class ComparePairListCellRenderer extends JLabel implements ListCellRenderer {
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+
+    public ComparePairListCellRenderer() {
+        setOpaque(true);
+    }
+    public Component getListCellRendererComponent(
+            JList list,
+            Object value,
+            int index,
+            boolean isSelected,
+            boolean cellHasFocus)
+    {
+        ComparePairType type = (ComparePairType)value;
+        setText(type.getDisplayName());
+        setBackground(isSelected ? BGCOLOR_SELECTED : Color.WHITE);
+        setForeground(Color.BLACK);
+        return this;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairType.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ComparePairType.java	(revision 1954)
@@ -0,0 +1,88 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * Enumeration of the possible comparison pairs
+ *
+ */
+public enum ComparePairType {
+
+    /**
+     * compare my version of an {@see OsmPrimitive} with their version
+     */
+    MY_WITH_THEIR (tr("My with Their"), new ListRole[] {MY_ENTRIES, THEIR_ENTRIES}),
+
+    /**
+     * compare my version of an {@see OsmPrimitive} with the merged version
+     */
+    MY_WITH_MERGED (tr("My with Merged"),  new ListRole[] {MY_ENTRIES, MERGED_ENTRIES}),
+
+    /**
+     * compare their version of an {@see OsmPrimitive} with the merged veresion
+     */
+    THEIR_WITH_MERGED(tr("Their with Merged"),  new ListRole[] {THEIR_ENTRIES, MERGED_ENTRIES});
+
+    /** the localized display name */
+    private final String displayName;
+    private ListRole[] participatingRoles;
+
+    ComparePairType(String displayName, ListRole[] participatingRoles) {
+        this.displayName = displayName;
+        this.participatingRoles = participatingRoles;
+    }
+
+    /**
+     * replies the display name
+     * 
+     * @return the display name
+     */
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    /**
+     * replies true, if <code>role</code> is participating in this comparison
+     * pair
+     * 
+     * @param role  the list role
+     * @return true, if <code>role</code> is participating in this comparison
+     * pair; false, otherwise
+     */
+    public boolean isParticipatingIn(ListRole role) {
+        for (ListRole r: participatingRoles) {
+            if (r.equals(role)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * replies the pair of {@see ListRole}s participating in this comparison
+     * pair
+     * 
+     * @return  the pair of list roles
+     */
+    public ListRole[] getParticipatingRoles() {
+        return participatingRoles;
+    }
+
+    /**
+     * replies the opposite role of <code>role</code> participating in this comparison
+     * pair
+     * 
+     * @param role one of the two roles in this pair
+     * @return the opposite role
+     * @exception IllegalStateException  if role is not participating in this pair
+     */
+    public ListRole getOppositeRole(ListRole role) {
+        if (!isParticipatingIn(role))
+            throw new IllegalStateException(tr("role {0} is not participating in compare pair {1}", role.toString(), this.toString()));
+        if (participatingRoles[0].equals(role))
+            return participatingRoles[1];
+        else
+            return participatingRoles[0];
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java	(revision 1954)
@@ -0,0 +1,327 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+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.VersionConflictResolveCommand;
+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.NodeListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger;
+import org.openstreetmap.josm.gui.conflict.pair.properties.OperationCancelledException;
+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.RelationMemberListMergeModel;
+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 {@see OsmPrimitive}s.
+ * 
+ * This component emits {@see PropertyChangeEvent}s for three properties:
+ * <ul>
+ *   <li>{@see #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is
+ *   completely resolved</li>
+ *   <li>{@see #MY_PRIMITIVE_PROP} - new value is the {@see OsmPrimitive} in the role of
+ *   my primitive</li>
+ *   <li>{@see #THEIR_PRIMITIVE_PROP} - new value is the {@see OsmPrimitive} in the role of
+ *   their primitive</li>
+ * </ul>
+ * 
+ */
+public class ConflictResolver extends JPanel implements PropertyChangeListener  {
+
+    /* -------------------------------------------------------------------------------------- */
+    /* Property names                                                                         */
+    /* -------------------------------------------------------------------------------------- */
+    /** name of the property indicating whether all conflicts are resolved,
+     *  {@see #isResolvedCompletely()}
+     */
+    static public final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely";
+    /**
+     * name of the property for the {@see OsmPrimitive} in the role "my"
+     */
+    static public final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive";
+
+    /**
+     * name of the property for the {@see OsmPrimitive} in the role "my"
+     */
+    static public final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive";
+
+
+    private static final Logger logger = Logger.getLogger(ConflictResolver.class.getName());
+
+    private JTabbedPane tabbedPane = null;
+    private TagMerger tagMerger;
+    private NodeListMerger nodeListMerger;
+    private RelationMemberMerger relationMemberMerger;
+    private PropertiesMerger propertiesMerger;
+    private OsmPrimitive my;
+    private OsmPrimitive their;
+
+    private ImageIcon mergeComplete;
+    private ImageIcon mergeIncomplete;
+
+    /** indicates whether the current conflict is resolved completely */
+    private boolean resolvedCompletely;
+
+    /**
+     * loads the required icons
+     */
+    protected void loadIcons() {
+        mergeComplete = ImageProvider.get("dialogs/conflict","mergecomplete.png" );
+        mergeIncomplete = ImageProvider.get("dialogs/conflict","mergeincomplete.png" );
+    }
+
+    /**
+     * builds the UI
+     */
+    protected 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);
+    }
+
+    /**
+     * constructor
+     */
+    public ConflictResolver() {
+        resolvedCompletely = false;
+        build();
+        loadIcons();
+    }
+
+    /**
+     * Sets the {@see 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 {@see 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
+     */
+    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, tr("Tags({0} conflicts)", newValue));
+                tabbedPane.setToolTipTextAt(1, tr("{0} pending tag conflicts to be resolved"));
+                tabbedPane.setIconAt(1, mergeIncomplete);
+            }
+            updateResolvedCompletely();
+        } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
+            boolean frozen = (Boolean)evt.getNewValue();
+            if (frozen && evt.getSource() == nodeListMerger.getModel()) {
+                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);
+            }
+            if (frozen && evt.getSource() == relationMemberMerger.getModel()) {
+                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();
+        }
+    }
+
+    /**
+     * populates the conflict resolver with the conflicts between my and their
+     * 
+     * @param my   my primitive (i.e. the primitive in the local dataset)
+     * @param their their primitive (i.e. the primitive in the server dataset)
+     * 
+     */
+    public void populate(OsmPrimitive my, OsmPrimitive their) {
+        setMy(my);
+        setTheir(their);
+        propertiesMerger.getModel().populate(my, their);
+        if (propertiesMerger.getModel().hasVisibleStateConflict()) {
+            tabbedPane.setEnabledAt(1, false);
+            tabbedPane.setEnabledAt(2, false);
+            tabbedPane.setEnabledAt(3, false);
+            return;
+        }
+        tabbedPane.setEnabledAt(0, true);
+        tagMerger.getModel().populate(my, their);
+        tabbedPane.setEnabledAt(1, true);
+
+        if (my instanceof Node) {
+            tabbedPane.setEnabledAt(2,false);
+            tabbedPane.setEnabledAt(3,false);
+        } else if (my instanceof Way) {
+            nodeListMerger.populate((Way)my, (Way)their);
+            tabbedPane.setEnabledAt(2, true);
+            tabbedPane.setEnabledAt(3, false);
+            tabbedPane.setTitleAt(3,tr("Members"));
+            tabbedPane.setIconAt(3, null);
+        } else if (my instanceof Relation) {
+            relationMemberMerger.populate((Relation)my, (Relation)their);
+            tabbedPane.setEnabledAt(2, false);
+            tabbedPane.setTitleAt(2,tr("Nodes"));
+            tabbedPane.setIconAt(2, null);
+            tabbedPane.setEnabledAt(3, true);
+        }
+        updateResolvedCompletely();
+    }
+
+    /**
+     * Builds the resolution command(s) for the resolved conflicts in this
+     * ConflictResolver
+     * 
+     * @return the resolution command
+     */
+    public Command buildResolveCommand() throws OperationCancelledException {
+        ArrayList<Command> commands = new ArrayList<Command>();
+        if (propertiesMerger.getModel().hasVisibleStateConflict()) {
+            if (propertiesMerger.getModel().isDecidedVisibleState()) {
+                commands.addAll(propertiesMerger.getModel().buildResolveCommand(my, their));
+            }
+        } else {
+            if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
+                commands.add(tagMerger.getModel().buildResolveCommand(my, their));
+            }
+            commands.addAll(propertiesMerger.getModel().buildResolveCommand(my, their));
+            if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
+                NodeListMergeModel model  =(NodeListMergeModel)nodeListMerger.getModel();
+                commands.add(model.buildResolveCommand((Way)my, (Way)their));
+            } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
+                RelationMemberListMergeModel model  =(RelationMemberListMergeModel)relationMemberMerger.getModel();
+                commands.add(model.buildResolveCommand((Relation)my, (Relation)their));
+            }
+            if (isResolvedCompletely()) {
+                commands.add(
+                        new VersionConflictResolveCommand(my, their)
+                );
+            }
+        }
+        return new SequenceCommand(tr("Conflict Resolution"), commands);
+    }
+
+    /**
+     * Updates the state of the property {@see #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;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMergeModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMergeModel.java	(revision 1954)
@@ -0,0 +1,787 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Observable;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableModel;
+
+/**
+ * ListMergeModel is a model for interactively comparing and merging two list of entries
+ * of type T. It maintains three lists of entries of type T:
+ * <ol>
+ *   <li>the list of <em>my</em> entries</li>
+ *   <li>the list of <em>their</em> entries</li>
+ *   <li>the list of <em>merged</em> entries</li>
+ * </ol>
+ * 
+ * A ListMergeModel is a factory for three {@see TableModel}s and three {@see ListSelectionModel}s:
+ * <ol>
+ *   <li>the table model and the list selection for for a  {@see JTable} which shows my entries.
+ *    See {@see #getMyTableModel()}</li> and {@see ListMergeModel#getMySelectionModel()}</li>
+ *   <li>dito for their entries and merged entries</li>
+ * </ol>
+ *
+ * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
+ * decisions. {@see PropertyChangeListener}s can register for property value changes of
+ * {@see #PROP_FROZEN}.
+ * 
+ * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
+ * <ul>
+ *   <li>{@see ListMergeModel#cloneEntryForMergedList(Object)} - clones an entry of type T</li>
+ *   <li>{@see ListMergeModel#isEqualEntry(Object, Object)} - checks whether two entries are equals </li>
+ *   <li>{@see ListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
+ *     a JTable, dispatched from {@see TableModel#setValueAt(Object, int, int)} </li>
+ * </ul>
+ * A ListMergeModel is used in combination with a {@see ListMerger}.
+ *
+ * @param <T>  the type of the list entries
+ * @see ListMerger
+ */
+public abstract class ListMergeModel<T> extends Observable {
+    private static final Logger logger = Logger.getLogger(ListMergeModel.class.getName());
+
+    public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen";
+
+    protected HashMap<ListRole, ArrayList<T>> entries;
+
+    protected DefaultTableModel myEntriesTableModel;
+    protected DefaultTableModel theirEntriesTableModel;
+    protected DefaultTableModel mergedEntriesTableModel;
+
+    protected EntriesSelectionModel myEntriesSelectionModel;
+    protected EntriesSelectionModel theirEntriesSelectionModel;
+    protected EntriesSelectionModel mergedEntriesSelectionModel;
+
+    private final ArrayList<PropertyChangeListener> listeners;
+    private boolean isFrozen = false;
+    private final ComparePairListModel comparePairListModel;
+
+
+
+    /**
+     * Creates a clone of an entry of type T suitable to be included in the
+     * list of merged entries
+     * 
+     * @param entry the entry
+     * @return the cloned entry
+     */
+    protected abstract T cloneEntryForMergedList(T entry);
+
+    /**
+     * checks whether two entries are equal. This is not necessarily the same as
+     * e1.equals(e2).
+     * 
+     * @param e1  the first entry
+     * @param e2  the second entry
+     * @return true, if the entries are equal, false otherwise.
+     */
+    public abstract boolean isEqualEntry(T e1, T e2);
+
+    /**
+     * Handles method dispatches from {@see TableModel#setValueAt(Object, int, int)}.
+     * 
+     * @param model the table model
+     * @param value  the value to be set
+     * @param row  the row index
+     * @param col the column index
+     * 
+     * @see TableModel#setValueAt(Object, int, int)
+     */
+    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
+
+
+
+    protected void buildMyEntriesTableModel() {
+        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
+    }
+
+    protected void buildTheirEntriesTableModel() {
+        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
+    }
+
+    protected void buildMergedEntriesTableModel() {
+        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
+    }
+
+    protected ArrayList<T> getMergedEntries() {
+        return entries.get(MERGED_ENTRIES);
+    }
+    protected ArrayList<T> getMyEntries() {
+        return entries.get(MY_ENTRIES);
+    }
+    protected ArrayList<T> getTheirEntries() {
+        return entries.get(THEIR_ENTRIES);
+    }
+
+    public int getMyEntriesSize() {
+        return getMyEntries().size();
+    }
+
+    public int getMergedEntriesSize() {
+        return getMergedEntries().size();
+    }
+
+    public int getTheirEntriesSize() {
+        return getTheirEntries().size();
+    }
+
+    public ListMergeModel() {
+        entries = new HashMap<ListRole, ArrayList<T>>();
+        for (ListRole role : ListRole.values()) {
+            entries.put(role, new ArrayList<T>());
+        }
+
+        buildMyEntriesTableModel();
+        buildTheirEntriesTableModel();
+        buildMergedEntriesTableModel();
+
+        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
+        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
+        mergedEntriesSelectionModel =  new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
+
+        listeners = new ArrayList<PropertyChangeListener>();
+        comparePairListModel = new ComparePairListModel();
+
+        setFrozen(true);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        synchronized(listeners) {
+            if (listener != null && ! listeners.contains(listener)) {
+                listeners.add(listener);
+            }
+        }
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        synchronized(listeners) {
+            if (listener != null && listeners.contains(listener)) {
+                listeners.remove(listener);
+            }
+        }
+    }
+
+    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
+        synchronized(listeners) {
+            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
+            for (PropertyChangeListener listener: listeners) {
+                listener.propertyChange(evt);
+            }
+        }
+    }
+
+    public void setFrozen(boolean isFrozen) {
+        boolean oldValue = this.isFrozen;
+        this.isFrozen = isFrozen;
+        fireFrozenChanged(oldValue, this.isFrozen);
+    }
+
+    public boolean isFrozen() {
+        return isFrozen;
+    }
+
+    public TableModel getMyTableModel() {
+        return myEntriesTableModel;
+    }
+
+    public TableModel getTheirTableModel() {
+        return theirEntriesTableModel;
+    }
+
+    public TableModel getMergedTableModel() {
+        return mergedEntriesTableModel;
+    }
+
+    public EntriesSelectionModel getMySelectionModel() {
+        return myEntriesSelectionModel;
+    }
+
+    public EntriesSelectionModel getTheirSelectionModel() {
+        return theirEntriesSelectionModel;
+    }
+
+    public EntriesSelectionModel getMergedSelectionModel() {
+        return mergedEntriesSelectionModel;
+    }
+
+    protected void fireModelDataChanged() {
+        myEntriesTableModel.fireTableDataChanged();
+        theirEntriesTableModel.fireTableDataChanged();
+        mergedEntriesTableModel.fireTableDataChanged();
+        setChanged();
+        notifyObservers();
+    }
+
+    protected void copyToTop(ListRole role, int []rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        for (int i = rows.length - 1; i >= 0; i--) {
+            int row = rows[i];
+            T n = entries.get(role).get(row);
+            entries.get(MERGED_ENTRIES).add(0, cloneEntryForMergedList(n));
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes at the top of the list of merged
+     * nodes.
+     * 
+     * @param rows the indices
+     */
+    public void copyMyToTop(int [] rows) {
+        copyToTop(MY_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes at the top of the list of merged
+     * nodes.
+     * 
+     * @param rows the indices
+     */
+    public void copyTheirToTop(int [] rows) {
+        copyToTop(THEIR_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes in source to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     * 
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     */
+
+    public void copyToEnd(ListRole source, int [] rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        ArrayList<T> mergedEntries = getMergedEntries();
+        for (int row : rows) {
+            T n = entries.get(source).get(row);
+            mergedEntries.add(cloneEntryForMergedList(n));
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.setSelectionInterval(mergedEntries.size()-rows.length, mergedEntries.size() -1);
+
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     * 
+     * @param rows the indices
+     */
+    public void copyMyToEnd(int [] rows) {
+        copyToEnd(MY_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     * 
+     * @param rows the indices
+     */
+    public void copyTheirToEnd(int [] rows) {
+        copyToEnd(THEIR_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     * 
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    protected void copyBeforeCurrent(ListRole source, int [] rows, int current) {
+        if (rows == null || rows.length == 0)
+            return;
+        ArrayList<T> mergedEntries = getMergedEntries();
+        if (current < 0 || current >= mergedEntries.size())
+            throw new IllegalArgumentException(tr("parameter current out of range: got {0}", current));
+        for (int i=rows.length -1; i>=0; i--) {
+            int row = rows[i];
+            T n = entries.get(source).get(row);
+            mergedEntries.add(current, cloneEntryForMergedList(n));
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     * 
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    public void copyMyBeforeCurrent(int [] rows, int current) {
+        copyBeforeCurrent(MY_ENTRIES,rows,current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     * 
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    public void copyTheirBeforeCurrent(int [] rows, int current) {
+        copyBeforeCurrent(THEIR_ENTRIES,rows,current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     * 
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    protected void copyAfterCurrent(ListRole source, int [] rows, int current) {
+        if (rows == null || rows.length == 0)
+            return;
+        ArrayList<T> mergedEntries = getMergedEntries();
+
+        if (current < 0 || current >= mergedEntries.size())
+            throw new IllegalArgumentException(tr("parameter current out of range: got {0}", current));
+        if (current == mergedEntries.size() -1) {
+            copyToEnd(source, rows);
+        } else {
+            for (int i=rows.length -1; i>=0; i--) {
+                int row = rows[i];
+                T n = entries.get(source).get(row);
+                mergedEntries.add(current+1, cloneEntryForMergedList(n));
+            }
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
+        notifyObservers();
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     * 
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    public void copyMyAfterCurrent(int [] rows, int current) {
+        copyAfterCurrent(MY_ENTRIES, rows, current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     * 
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
+     * 
+     */
+    public void copyTheirAfterCurrent(int [] rows, int current) {
+        copyAfterCurrent(THEIR_ENTRIES, rows, current);
+    }
+
+    /**
+     * Moves the nodes given by indices in rows  up by one position in the list
+     * of merged nodes.
+     * 
+     * @param rows the indices
+     * 
+     */
+    public void moveUpMerged(int [] rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        if (rows[0] == 0)
+            // can't move up
+            return;
+        ArrayList<T> mergedEntries = getMergedEntries();
+        for (int row: rows) {
+            T n = mergedEntries.get(row);
+            mergedEntries.remove(row);
+            mergedEntries.add(row -1, n);
+        }
+        fireModelDataChanged();
+        notifyObservers();
+        mergedEntriesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
+        }
+    }
+
+    /**
+     * Moves the nodes given by indices in rows down by one position in the list
+     * of merged nodes.
+     * 
+     * @param rows the indices
+     */
+    public void moveDownMerged(int [] rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        ArrayList<T> mergedEntries = getMergedEntries();
+        if (rows[rows.length -1] == mergedEntries.size() -1)
+            // can't move down
+            return;
+        for (int i = rows.length-1; i>=0;i--) {
+            int row = rows[i];
+            T n = mergedEntries.get(row);
+            mergedEntries.remove(row);
+            mergedEntries.add(row +1, n);
+        }
+        fireModelDataChanged();
+        notifyObservers();
+        mergedEntriesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
+        }
+    }
+
+    /**
+     * Removes the nodes given by indices in rows from the list
+     * of merged nodes.
+     * 
+     * @param rows the indices
+     */
+    public void removeMerged(int [] rows) {
+        if (rows == null || rows.length == 0)
+            return;
+
+        ArrayList<T> mergedEntries = getMergedEntries();
+
+        for (int i = rows.length-1; i>=0;i--) {
+            mergedEntries.remove(rows[i]);
+        }
+        fireModelDataChanged();
+        notifyObservers();
+        mergedEntriesSelectionModel.clearSelection();
+    }
+
+
+    /**
+     * Replies true if the list of my entries and the list of their
+     * entries are equal
+     * 
+     * @return true, if the lists are equal; false otherwise
+     */
+    protected boolean myAndTheirEntriesEqual() {
+
+        if (getMyEntries().size() != getTheirEntries().size())
+            return false;
+        for (int i=0; i < getMyEntries().size(); i++) {
+            if (! isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
+                return false;
+        }
+        return true;
+    }
+
+
+    /**
+     * This an adapter between a {@see JTable} and one of the three entry lists
+     * in the role {@see ListRole} managed by the {@see ListMergeModel}.
+     * 
+     * From the point of view of the {@see JTable} it is a {@see TableModel}.
+     *
+     * @param <T>
+     * @see ListMergeModel#getMyTableModel()
+     * @see ListMergeModel#getTheirTableModel()
+     * @see ListMergeModel#getMergedTableModel()
+     */
+    public class EntriesTableModel extends DefaultTableModel {
+        private final ListRole role;
+
+        /**
+         * 
+         * @param role the role
+         */
+        public EntriesTableModel(ListRole role) {
+            this.role = role;
+        }
+
+        @Override
+        public int getRowCount() {
+            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
+            count = Math.max(count, getTheirEntries().size());
+            return count;
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            if (row < entries.get(role).size())
+                return entries.get(role).get(row);
+            return null;
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+
+        @Override
+        public void setValueAt(Object value, int row, int col) {
+            ListMergeModel.this.setValueAt(this, value,row,col);
+        }
+
+        public ListMergeModel getListMergeModel() {
+            return ListMergeModel.this;
+        }
+
+        /**
+         * replies true if the {@see ListRole} of this {@see EntriesTableModel}
+         * participates in the current {@see ComparePairType}
+         * 
+         * @return true, if the if the {@see ListRole} of this {@see EntriesTableModel}
+         * participates in the current {@see ComparePairType}
+         * 
+         * @see ComparePairListModel#getSelectedComparePair()
+         */
+        public boolean isParticipatingInCurrentComparePair() {
+            return getComparePairListModel()
+            .getSelectedComparePair()
+            .isParticipatingIn(role);
+        }
+
+        /**
+         * replies true if the entry at <code>row</code> is equal to the entry at the
+         * same position in the opposite list of the current {@see ComparePairType}.
+         * 
+         * @param row  the row number
+         * @return true if the entry at <code>row</code> is equal to the entry at the
+         * same position in the opposite list of the current {@see ComparePairType}
+         * @exception IllegalStateException thrown, if this model is not participating in the
+         *   current  {@see ComparePairType}
+         * @see ComparePairType#getOppositeRole(ListRole)
+         * @see #getRole()
+         * @see #getOppositeEntries()
+         */
+        public boolean isSamePositionInOppositeList(int row) {
+            if (!isParticipatingInCurrentComparePair())
+                throw new IllegalStateException(tr("list in role {0} is currently not participating in a compare pair", role.toString()));
+            if (row >= getEntries().size()) return false;
+            if (row >= getOppositeEntries().size()) return false;
+
+            T e1 = getEntries().get(row);
+            T e2 = getOppositeEntries().get(row);
+            return isEqualEntry(e1, e2);
+        }
+
+        /**
+         * replies true if the entry at the current position is present in the opposite list
+         * of the current {@see ComparePairType}.
+         * 
+         * @param row the current row
+         * @return true if the entry at the current position is present in the opposite list
+         * of the current {@see ComparePairType}.
+         * @exception IllegalStateException thrown, if this model is not participating in the
+         *   current  {@see ComparePairType}
+         * @see ComparePairType#getOppositeRole(ListRole)
+         * @see #getRole()
+         * @see #getOppositeEntries()
+         */
+        public boolean isIncludedInOppositeList(int row) {
+            if (!isParticipatingInCurrentComparePair())
+                throw new IllegalStateException(tr("list in role {0} is currently not participating in a compare pair", role.toString()));
+
+            if (row >= getEntries().size()) return false;
+            T e1 = getEntries().get(row);
+            for (T e2: getOppositeEntries()) {
+                if (isEqualEntry(e1, e2)) return true;
+            }
+            return false;
+        }
+
+        protected ArrayList<T> getEntries() {
+            return entries.get(role);
+        }
+
+        /**
+         * replies the opposite list of entries with respect to the current {@see ComparePairType}
+         * 
+         * @return the opposite list of entries
+         */
+        protected ArrayList<T> getOppositeEntries() {
+            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
+            return entries.get(opposite);
+        }
+
+        public ListRole getRole() {
+            return role;
+        }
+    }
+
+    /**
+     * This is the selection model to be used in a {@see JTable} which displays
+     * an entry list managed by {@see ListMergeModel}.
+     * 
+     * The model ensures that only rows displaying an entry in the entry list
+     * can be selected. "Empty" rows can't be selected.
+     * 
+     * @see ListMergeModel#getMySelectionModel()
+     * @see ListMergeModel#getMergedSelectionModel()
+     * @see ListMergeModel#getTheirSelectionModel()
+     *
+     */
+    protected class EntriesSelectionModel extends DefaultListSelectionModel {
+        private final ArrayList<T> entries;
+
+        public EntriesSelectionModel(ArrayList<T> nodes) {
+            this.entries = nodes;
+        }
+
+        @Override
+        public void addSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            if (index0 > entries.size() - 1) return;
+            index0 = Math.min(entries.size()-1, index0);
+            index1 = Math.min(entries.size()-1, index1);
+            super.addSelectionInterval(index0, index1);
+        }
+
+        @Override
+        public void insertIndexInterval(int index, int length, boolean before) {
+            if (entries.isEmpty()) return;
+            if (before) {
+                int newindex = Math.min(entries.size()-1, index);
+                if (newindex < index - length) return;
+                length = length - (index - newindex);
+                super.insertIndexInterval(newindex, length, before);
+            } else {
+                if (index > entries.size() -1) return;
+                length = Math.min(entries.size()-1 - index, length);
+                super.insertIndexInterval(index, length, before);
+            }
+        }
+
+        @Override
+        public void moveLeadSelectionIndex(int leadIndex) {
+            if (entries.isEmpty()) return;
+            leadIndex = Math.max(0, leadIndex);
+            leadIndex = Math.min(entries.size() - 1, leadIndex);
+            super.moveLeadSelectionIndex(leadIndex);
+        }
+
+        @Override
+        public void removeIndexInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+            super.removeIndexInterval(index0, index1);
+        }
+
+        @Override
+        public void removeSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+            super.removeSelectionInterval(index0, index1);
+        }
+
+        @Override
+        public void setAnchorSelectionIndex(int anchorIndex) {
+            if (entries.isEmpty()) return;
+            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
+            super.setAnchorSelectionIndex(anchorIndex);
+        }
+
+        @Override
+        public void setLeadSelectionIndex(int leadIndex) {
+            if (entries.isEmpty()) return;
+            leadIndex = Math.min(entries.size() - 1, leadIndex);
+            super.setLeadSelectionIndex(leadIndex);
+        }
+
+        @Override
+        public void setSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+
+            super.setSelectionInterval(index0, index1);
+        }
+    }
+
+    public ComparePairListModel getComparePairListModel() {
+        return this.comparePairListModel;
+    }
+
+    public class ComparePairListModel extends AbstractListModel implements ComboBoxModel {
+
+        private  int selectedIdx;
+        private final ArrayList<ComparePairType> compareModes;
+
+        public ComparePairListModel() {
+            this.compareModes = new ArrayList<ComparePairType>();
+            compareModes.add(MY_WITH_THEIR);
+            compareModes.add(MY_WITH_MERGED);
+            compareModes.add(THEIR_WITH_MERGED);
+            selectedIdx = 0;
+        }
+
+        public Object getElementAt(int index) {
+            if (index < compareModes.size())
+                return compareModes.get(index);
+            throw new IllegalArgumentException(tr("unexpected value of parameter \"index\". Got {0}", index));
+        }
+
+        public int getSize() {
+            return compareModes.size();
+        }
+
+        public Object getSelectedItem() {
+            return compareModes.get(selectedIdx);
+        }
+
+        public void setSelectedItem(Object anItem) {
+            int i = compareModes.indexOf(anItem);
+            if (i < 0)
+                throw new IllegalStateException(tr("item {0} not found in list", anItem));
+            selectedIdx = i;
+            fireModelDataChanged();
+        }
+
+        public ComparePairType getSelectedComparePair() {
+            return compareModes.get(selectedIdx);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMerger.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMerger.java	(revision 1954)
@@ -0,0 +1,964 @@
+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.Adjustable;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * A UI component for resolving conflicts in two lists of entries of type T.
+ *
+ * @param T  the type of the entries
+ * @see ListMergeModel
+ */
+public abstract class ListMerger<T> extends JPanel implements PropertyChangeListener, Observer {
+    private static final Logger logger = Logger.getLogger(ListMerger.class.getName());
+
+    protected JTable myEntriesTable;
+    protected JTable mergedEntriesTable;
+    protected JTable theirEntriesTable;
+
+    protected ListMergeModel<T> model;
+
+    private CopyStartLeftAction copyStartLeftAction;
+    private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
+    private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
+    private CopyEndLeftAction copyEndLeftAction;
+
+    private CopyStartRightAction copyStartRightAction;
+    private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
+    private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
+    private CopyEndRightAction copyEndRightAction;
+
+    private MoveUpMergedAction moveUpMergedAction;
+    private MoveDownMergedAction moveDownMergedAction;
+    private RemoveMergedAction removeMergedAction;
+    private FreezeAction freezeAction;
+
+    private AdjustmentSynchronizer adjustmentSynchronizer;
+
+    private  JCheckBox cbLockMyScrolling;
+    private  JCheckBox cbLockMergedScrolling;
+    private  JCheckBox cbLockTheirScrolling;
+
+    private  JLabel lblMyVersion;
+    private  JLabel lblMergedVersion;
+    private  JLabel lblTheirVersion;
+
+
+    private  JLabel lblFrozenState;
+
+    abstract protected JScrollPane buildMyElementsTable();
+    abstract protected JScrollPane buildMergedElementsTable();
+    abstract protected JScrollPane buildTheirElementsTable();
+
+    protected JScrollPane embeddInScrollPane(JTable table) {
+        JScrollPane pane = new JScrollPane(table);
+        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        if (adjustmentSynchronizer == null) {
+            adjustmentSynchronizer = new AdjustmentSynchronizer();
+        }
+        return pane;
+    }
+
+    protected void wireActionsToSelectionModels() {
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
+
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
+
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
+    }
+
+    protected JPanel buildLeftButtonPanel() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        copyStartLeftAction = new CopyStartLeftAction();
+        JButton btn = new JButton(copyStartLeftAction);
+        btn.setName("button.copystartleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
+        btn = new JButton(copyBeforeCurrentLeftAction);
+        btn.setName("button.copybeforecurrentleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 2;
+        copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
+        btn = new JButton(copyAfterCurrentLeftAction);
+        btn.setName("button.copyaftercurrentleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 3;
+        copyEndLeftAction = new CopyEndLeftAction();
+        btn = new JButton(copyEndLeftAction);
+        btn.setName("button.copyendleft");
+        pnl.add(btn, gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildRightButtonPanel() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        copyStartRightAction = new CopyStartRightAction();
+        pnl.add(new JButton(copyStartRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
+        pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 2;
+        copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
+        pnl.add(new JButton(copyAfterCurrentRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 3;
+        copyEndRightAction = new CopyEndRightAction();
+        pnl.add(new JButton(copyEndRightAction), gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildMergedListControlButtons() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.3;
+        gc.weighty = 0.0;
+        moveUpMergedAction = new MoveUpMergedAction();
+        pnl.add(new JButton(moveUpMergedAction), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 0;
+        moveDownMergedAction = new MoveDownMergedAction();
+        pnl.add(new JButton(moveDownMergedAction), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 0;
+        removeMergedAction = new RemoveMergedAction();
+        pnl.add(new JButton(removeMergedAction), gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
+        JPanel panel = new JPanel();
+        panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+        panel.add(new JLabel(tr("lock scrolling")));
+        panel.add(cb);
+        return panel;
+    }
+
+    protected JPanel buildComparePairSelectionPanel() {
+        JPanel p = new JPanel();
+        p.setLayout(new FlowLayout(FlowLayout.LEFT));
+        p.add(new JLabel(tr("Compare ")));
+        JComboBox cbComparePair =new JComboBox(model.getComparePairListModel());
+        cbComparePair.setRenderer(new ComparePairListCellRenderer());
+        p.add(cbComparePair);
+        return p;
+    }
+
+    protected JPanel buildFrozeStateControlPanel() {
+        JPanel p = new JPanel();
+        p.setLayout(new FlowLayout(FlowLayout.LEFT));
+        lblFrozenState = new JLabel();
+        p.add(lblFrozenState);
+        freezeAction = new FreezeAction();
+        JToggleButton btn = new JToggleButton(freezeAction);
+        freezeAction.adapt(btn);
+        btn.setName("button.freeze");
+        p.add(btn);
+
+        return p;
+    }
+
+    protected void build() {
+        setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        // ------------------
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(10,0,0,0);
+        lblMyVersion = new JLabel(tr("My version"));
+        lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
+        add(lblMyVersion, gc);
+
+        gc.gridx = 2;
+        gc.gridy = 0;
+        lblMergedVersion = new JLabel(tr("Merged version"));
+        lblMergedVersion.setToolTipText(tr("List of merged elements. They will replace the my elements when the merge decisions are applied."));
+        add(lblMergedVersion, gc);
+
+        gc.gridx = 4;
+        gc.gridy = 0;
+        lblTheirVersion = new JLabel(tr("Their version"));
+        lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
+        add(lblTheirVersion, gc);
+
+        // ------------------------------
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,0,0,0);
+        cbLockMyScrolling = new JCheckBox();
+        cbLockMyScrolling.setName("checkbox.lockmyscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 1;
+        cbLockMergedScrolling = new JCheckBox();
+        cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 1;
+        cbLockTheirScrolling = new JCheckBox();
+        cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
+
+        // --------------------------------
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 1.0;
+        gc.insets = new Insets(0,0,0,0);
+        JScrollPane pane = buildMyElementsTable();
+        adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        gc.gridx = 1;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildLeftButtonPanel(), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        pane = buildMergedElementsTable();
+        adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        gc.gridx = 3;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildRightButtonPanel(), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        pane = buildTheirElementsTable();
+        adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        // ----------------------------------
+        gc.gridx = 2;
+        gc.gridy = 3;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildMergedListControlButtons(), gc);
+
+        // -----------------------------------
+        gc.gridx = 0;
+        gc.gridy = 4;
+        gc.gridwidth = 2;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildComparePairSelectionPanel(), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 4;
+        gc.gridwidth = 3;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildFrozeStateControlPanel(), gc);
+
+
+        wireActionsToSelectionModels();
+    }
+
+    public ListMerger(ListMergeModel<T> model) {
+        this.model = model;
+        model.addObserver(this);
+        build();
+        model.addPropertyChangeListener(this);
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
+     *
+     */
+    class CopyStartLeftAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyStartLeftAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("> top"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes to the start of the merged node list"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = myEntriesTable.getSelectedRows();
+            model.copyMyToTop(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes at the end of the list of merged nodes.
+     *
+     */
+    class CopyEndLeftAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyEndLeftAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("> bottom"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements to the end of the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = myEntriesTable.getSelectedRows();
+            model.copyMyToEnd(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
+     *
+     */
+    class CopyBeforeCurrentLeftAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyBeforeCurrentLeftAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "> before");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements before the first selected element in the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = myEntriesTable.getSelectedRows();
+            int [] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int current = mergedRows[0];
+            model.copyMyBeforeCurrent(myRows, current);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
+     *
+     */
+    class CopyAfterCurrentLeftAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyAfterCurrentLeftAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "> after");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements after the first selected element in the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = myEntriesTable.getSelectedRows();
+            int [] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int current = mergedRows[0];
+            model.copyMyAfterCurrent(myRows, current);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+
+    class CopyStartRightAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyStartRightAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< top");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element to the start of the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = theirEntriesTable.getSelectedRows();
+            model.copyTheirToTop(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+
+    class CopyEndRightAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyEndRightAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< bottom");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements to the end of the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = theirEntriesTable.getSelectedRows();
+            model.copyTheirToEnd(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    class CopyBeforeCurrentRightAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyBeforeCurrentRightAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< before");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements before the first selected element in the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = theirEntriesTable.getSelectedRows();
+            int [] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int current = mergedRows[0];
+            model.copyTheirBeforeCurrent(myRows, current);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+
+    class CopyAfterCurrentRightAction extends AbstractAction implements ListSelectionListener {
+
+        public CopyAfterCurrentRightAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< after");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element after the first selected element in the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = theirEntriesTable.getSelectedRows();
+            int [] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int current = mergedRows[0];
+            model.copyTheirAfterCurrent(myRows, current);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+
+    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
+
+        public MoveUpMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Up"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected elements by one position"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            model.moveUpMerged(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+                    && rows[0] != 0
+            );
+        }
+    }
+
+    /**
+     * Action for moving the currently selected entries in the list of merged entries
+     * one position down
+     *
+     */
+    class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
+
+        public MoveDownMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Down"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            model.moveDownMerged(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
+            );
+        }
+    }
+
+    /**
+     * Action for removing the selected entries in the list of merged entries
+     * from the list of merged entries.
+     *
+     */
+    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
+
+        public RemoveMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Remove"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            model.removeMerged(rows);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+            );
+        }
+    }
+
+    static public interface FreezeActionProperties {
+        String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
+    }
+
+    /**
+     * Action for freezing the current state of the list merger
+     *
+     */
+    class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties  {
+
+        public FreezeAction() {
+            putValue(Action.NAME, tr("Freeze"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements"));
+            putValue(PROP_SELECTED, false);
+            setEnabled(true);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            // do nothing
+        }
+
+        /**
+         * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
+         * such that the action gets notified about item state changes and the button gets
+         * notified about selection state changes of the action.
+         *
+         * @param btn a toggle button
+         */
+        public void adapt(final JToggleButton btn) {
+            btn.addItemListener(this);
+            addPropertyChangeListener(
+                    new PropertyChangeListener() {
+                        public void propertyChange(PropertyChangeEvent evt) {
+                            if (evt.getPropertyName().equals(PROP_SELECTED)) {
+                                btn.setSelected((Boolean)evt.getNewValue());
+                            }
+                        }
+                    }
+            );
+        }
+
+        public void itemStateChanged(ItemEvent e) {
+            int state = e.getStateChange();
+            if (state == ItemEvent.SELECTED) {
+                putValue(Action.NAME, tr("Unfreeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging"));
+                model.setFrozen(true);
+            } else if (state == ItemEvent.DESELECTED) {
+                putValue(Action.NAME, tr("Freeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements"));
+                model.setFrozen(false);
+            }
+            boolean isSelected = (Boolean)getValue(PROP_SELECTED);
+            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
+                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
+            }
+
+        }
+    }
+
+    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
+        myEntriesTable.getSelectionModel().clearSelection();
+        myEntriesTable.setEnabled(!newValue);
+        theirEntriesTable.getSelectionModel().clearSelection();
+        theirEntriesTable.setEnabled(!newValue);
+        mergedEntriesTable.getSelectionModel().clearSelection();
+        mergedEntriesTable.setEnabled(!newValue);
+        if (freezeAction != null) {
+            freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
+        }
+        if (newValue) {
+            lblFrozenState.setText(
+                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries</html>",
+                            freezeAction.getValue(Action.NAME))
+            );
+        } else {
+            lblFrozenState.setText(
+                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries</html>",
+                            freezeAction.getValue(Action.NAME))
+            );
+        }
+    }
+
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
+            handlePropertyChangeFrozen((Boolean)evt.getOldValue(), (Boolean)evt.getNewValue());
+        }
+    }
+
+    public ListMergeModel<T> getModel() {
+        return model;
+    }
+
+
+    public void update(Observable o, Object arg) {
+        lblMyVersion.setText(
+                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
+        );
+        lblMergedVersion.setText(
+                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
+        );
+        lblTheirVersion.setText(
+                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
+        );
+    }
+
+    /**
+     * Synchronizes scrollbar adjustments between a set of
+     * {@see Adjustable}s. Whenever the adjustment of one of
+     * the registerd Adjustables is updated the adjustment of
+     * the other registered Adjustables is adjusted too.
+     *
+     */
+    class AdjustmentSynchronizer implements AdjustmentListener {
+
+        private final  ArrayList<Adjustable> synchronizedAdjustables;
+        private final  HashMap<Adjustable, Boolean> enabledMap;
+
+        private final Observable observable;
+
+        public AdjustmentSynchronizer() {
+            synchronizedAdjustables = new ArrayList<Adjustable>();
+            enabledMap = new HashMap<Adjustable, Boolean>();
+            observable = new Observable();
+        }
+
+
+        /**
+         * registers an {@see Adjustable} for participation in synchronized
+         * scrolling.
+         *
+         * @param adjustable the adjustable
+         */
+        public void participateInSynchronizedScrolling(Adjustable adjustable) {
+            if (adjustable == null)
+                return;
+            if (synchronizedAdjustables.contains(adjustable))
+                return;
+            synchronizedAdjustables.add(adjustable);
+            setParticipatingInSynchronizedScrolling(adjustable, true);
+            adjustable.addAdjustmentListener(this);
+        }
+
+        /**
+         * event handler for {@see AdjustmentEvent}s
+         *
+         */
+        public void adjustmentValueChanged(AdjustmentEvent e) {
+            if (! enabledMap.get(e.getAdjustable()))
+                return;
+            for (Adjustable a : synchronizedAdjustables) {
+                if (a != e.getAdjustable() && isParticipatingInSynchronizedScrolling(a)) {
+                    a.setValue(e.getValue());
+                }
+            }
+        }
+
+        /**
+         * sets whether adjustable participates in adjustment synchronization
+         * or not
+         *
+         * @param adjustable the adjustable
+         */
+        protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) {
+            if (adjustable == null)
+                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "adjustable"));
+
+            if (! synchronizedAdjustables.contains(adjustable))
+                throw new IllegalStateException(tr("adjustable {0} not registered yet. Can't set participation in synchronized adjustment",adjustable));
+
+            enabledMap.put(adjustable, isParticipating);
+            observable.notifyObservers();
+        }
+
+        /**
+         * returns true if an adjustable is participating in synchronized scrolling
+         *
+         * @param adjustable the adjustable
+         * @return true, if the adjustable is participating in synchronized scrolling, false otherwise
+         * @throws IllegalStateException thrown, if adjustable is not registered for synchronized scrolling
+         */
+        protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) throws IllegalStateException {
+            if (! synchronizedAdjustables.contains(adjustable))
+                throw new IllegalStateException(tr("adjustable {0} not registered yet",adjustable));
+
+            return enabledMap.get(adjustable);
+        }
+
+        /**
+         * wires a {@see JCheckBox} to  the adjustment synchronizer, in such a way  that:
+         * <li>
+         *   <ol>state changes in the checkbox control whether the adjustable participates
+         *      in synchronized adjustment</ol>
+         *   <ol>state changes in this {@see AdjustmentSynchronizer} are reflected in the
+         *      {@see JCheckBox}</ol>
+         * </li>
+         *
+         *
+         * @param view  the checkbox to control whether an adjustable participates in synchronized
+         *      adjustment
+         * @param adjustable the adjustable
+         * @exception IllegalArgumentException thrown, if view is null
+         * @exception IllegalArgumentException thrown, if adjustable is null
+         */
+        protected void adapt(final JCheckBox view, final Adjustable adjustable) throws IllegalArgumentException, IllegalStateException {
+            if (adjustable == null)
+                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "adjustable"));
+            if (view == null)
+                throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "view"));
+
+            if (! synchronizedAdjustables.contains(adjustable)) {
+                participateInSynchronizedScrolling(adjustable);
+            }
+
+            // register an item lister with the check box
+            //
+            view.addItemListener(new ItemListener() {
+                public void itemStateChanged(ItemEvent e) {
+                    switch(e.getStateChange()) {
+                    case ItemEvent.SELECTED:
+                        if (!isParticipatingInSynchronizedScrolling(adjustable)) {
+                            setParticipatingInSynchronizedScrolling(adjustable, true);
+                        }
+                        break;
+                    case ItemEvent.DESELECTED:
+                        if (isParticipatingInSynchronizedScrolling(adjustable)) {
+                            setParticipatingInSynchronizedScrolling(adjustable, false);
+                        }
+                        break;
+                    }
+                }
+            });
+
+
+            observable.addObserver(
+                    new Observer() {
+                        public void update(Observable o, Object arg) {
+                            boolean sync = isParticipatingInSynchronizedScrolling(adjustable);
+                            if (view.isSelected() != sync) {
+                                view.setSelected(sync);
+                            }
+                        }
+                    }
+            );
+            setParticipatingInSynchronizedScrolling(adjustable, true);
+            view.setSelected(true);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java	(revision 1954)
@@ -0,0 +1,11 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+/**
+ * Enumeration of roles entry lists play in {@see ListMergeModel}
+ */
+public enum ListRole {
+    MY_ENTRIES,
+    MERGED_ENTRIES,
+    THEIR_ENTRIES
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/MergeDecisionType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/MergeDecisionType.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/MergeDecisionType.java	(revision 1954)
@@ -0,0 +1,8 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+public enum MergeDecisionType {
+    KEEP_MINE,
+    KEEP_THEIR,
+    UNDECIDED,
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListColumnModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListColumnModel.java	(revision 1954)
@@ -0,0 +1,36 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.nodes;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+
+public class NodeListColumnModel extends DefaultTableColumnModel {
+
+    protected void createColumns(TableCellRenderer renderer) {
+
+        TableColumn col = null;
+
+        // column 0 - Row num
+        col = new TableColumn(0);
+        col.setHeaderValue("");
+        col.setResizable(true);
+        col.setWidth(30);
+        col.setPreferredWidth(30);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+
+        // column 1 - Node
+        col = new TableColumn(1);
+        col.setHeaderValue(tr("Node"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+    }
+
+    public NodeListColumnModel(TableCellRenderer renderer) {
+        createColumns(renderer);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java	(revision 1954)
@@ -0,0 +1,93 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.nodes;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import javax.swing.table.DefaultTableModel;
+
+import org.openstreetmap.josm.command.WayNodesConflictResolverCommand;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.ListRole;
+
+public class NodeListMergeModel extends ListMergeModel<Node>{
+
+    private static final Logger logger = Logger.getLogger(NodeListMergeModel.class.getName());
+
+
+    /**
+     * Populates the model with the nodes in the two {@see Way}s <code>my</code> and
+     * <code>their</code>.
+     *
+     * @param my  my way (i.e. the way in the local dataset)
+     * @param their their way (i.e. the way in the server dataset)
+     * @exception IllegalArgumentException thrown, if my is null
+     * @exception IllegalArgumentException  thrown, if their is null
+     */
+    public void populate(Way my, Way their) {
+        if (my == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "my"));
+        if (their == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "their"));
+        getMergedEntries().clear();
+        getMyEntries().clear();
+        getTheirEntries().clear();
+        for (Node n : my.getNodes()) {
+            getMyEntries().add(n);
+        }
+        for (Node n : their.getNodes()) {
+            getTheirEntries().add(n);
+        }
+        if (myAndTheirEntriesEqual()) {
+            entries.put(ListRole.MERGED_ENTRIES, new ArrayList<Node>(getMyEntries()));
+            setFrozen(true);
+        } else {
+            setFrozen(false);
+        }
+
+        fireModelDataChanged();
+    }
+
+    /**
+     * Builds the command to resolve conflicts in the node list of a way
+     *
+     * @param my  my way. Must not be null.
+     * @param their  their way. Must not be null
+     * @return the command
+     * @exception IllegalArgumentException thrown, if my is null or not a {@see Way}
+     * @exception IllegalArgumentException thrown, if their is null or not a {@see Way}
+     * @exception IllegalStateException thrown, if the merge is not yet frozen
+     */
+    public WayNodesConflictResolverCommand buildResolveCommand(Way my, Way their) {
+        if (my == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "my"));
+        if (their == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "their"));
+        if (! isFrozen())
+            throw new IllegalArgumentException(tr("Merged nodes not frozen yet. Can't build resolution command"));
+        return new WayNodesConflictResolverCommand(my, their, getMergedEntries());
+    }
+
+
+    @Override
+    public boolean isEqualEntry(Node e1, Node e2) {
+        if (e1.id > 0)
+            return e1.id == e2.id;
+        else
+            return e1 == e2;
+    }
+
+    @Override
+    protected void setValueAt(DefaultTableModel model, Object value, int row, int col) {
+        // do nothing - node list tables are not editable
+    }
+
+    @Override
+    protected Node cloneEntryForMergedList(Node entry) {
+        return entry;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java	(revision 1954)
@@ -0,0 +1,70 @@
+package org.openstreetmap.josm.gui.conflict.pair.nodes;
+
+import java.util.logging.Logger;
+
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.conflict.pair.ListMerger;
+
+/**
+ * A UI component for resolving conflicts in the node lists of two {@see Way}s.
+ * 
+ */
+public class NodeListMerger extends ListMerger<Node> {
+    private static final Logger logger = Logger.getLogger(NodeListMerger.class.getName());
+
+
+    public NodeListMerger() {
+        super(new NodeListMergeModel());
+    }
+
+    @Override
+    protected JScrollPane buildMyElementsTable() {
+        myEntriesTable  = new JTable(
+                model.getMyTableModel(),
+                new NodeListColumnModel(
+                        new NodeListTableCellRenderer()
+                ),
+                model.getMySelectionModel()
+        );
+        myEntriesTable.setName("table.mynodes");
+        myEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(myEntriesTable);
+    }
+
+    @Override
+    protected JScrollPane buildMergedElementsTable() {
+        mergedEntriesTable  = new JTable(
+                model.getMergedTableModel(),
+                new NodeListColumnModel(
+                        new NodeListTableCellRenderer()
+                ),
+                model.getMergedSelectionModel()
+        );
+        mergedEntriesTable.setName("table.mergednodes");
+        mergedEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(mergedEntriesTable);
+    }
+
+    @Override
+    protected JScrollPane buildTheirElementsTable() {
+        theirEntriesTable  = new JTable(
+                model.getTheirTableModel(),
+                new NodeListColumnModel(
+                        new NodeListTableCellRenderer()
+                ),
+                model.getTheirSelectionModel()
+        );
+        theirEntriesTable.setName("table.theirnodes");
+        theirEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(theirEntriesTable);
+    }
+
+
+    public void populate(Way my, Way their) {
+        ((NodeListMergeModel)model).populate(my, their);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,195 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.nodes;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.logging.Logger;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.border.Border;
+import javax.swing.table.TableCellRenderer;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This is the {@see TableCellRenderer} used in the node tables of {@see NodeListMerger}.
+ * 
+ */
+public  class NodeListTableCellRenderer extends JLabel implements TableCellRenderer {
+    static private final Logger logger = Logger.getLogger(NodeListTableCellRenderer.class.getName());
+    private static DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000");
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+    public final static Color BGCOLOR_EMPTY_ROW = new Color(234,234,234);
+    public final static Color BGCOLOR_FROZEN = new Color(234,234,234);
+    public final static Color BGCOLOR_PARTICIPAING_IN_COMPARISON = Color.BLACK;
+    public final static Color FGCOLOR_PARTICIPAING_IN_COMPARISON = Color.WHITE;
+
+    public final static Color BGCOLOR_NOT_IN_OPPOSITE = new Color(255,197,197);
+    public final static Color BGCOLOR_IN_OPPOSITE = new Color(255,234,213);
+    public final static Color BGCOLOR_SAME_POSITION_IN_OPPOSITE = new Color(217,255,217);
+
+    private final ImageIcon icon;
+    private final Border rowNumberBorder;
+
+    /**
+     * constructor
+     */
+    public NodeListTableCellRenderer() {
+        icon = ImageProvider.get("data", "node");
+        rowNumberBorder = BorderFactory.createEmptyBorder(0,4,0,0);
+        setOpaque(true);
+    }
+
+    /**
+     * build the tool tip text for an {@see OsmPrimitive}. It consist of the formatted
+     * key/value pairs for this primitive.
+     * 
+     * @param primitive
+     * @return the tool tip text
+     */
+    public String buildToolTipText(OsmPrimitive primitive) {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("<html>");
+        // show the id
+        //
+        sb.append("<strong>id</strong>=")
+        .append(primitive.id)
+        .append("<br>");
+
+        // show the key/value-pairs, sorted by key
+        //
+        ArrayList<String> keyList = new ArrayList<String>(primitive.keySet());
+        Collections.sort(keyList);
+        for (int i = 0; i < keyList.size(); i++) {
+            if (i > 0) {
+                sb.append("<br>");
+            }
+            String key = keyList.get(i);
+            sb.append("<strong>")
+            .append(key)
+            .append("</strong>")
+            .append("=");
+            // make sure long values are split into several rows. Otherwise
+            // the tool tip window can become to wide
+            //
+            String value = primitive.get(key);
+            while(value.length() != 0) {
+                sb.append(value.substring(0,Math.min(50, value.length())));
+                if (value.length() > 50) {
+                    sb.append("<br>");
+                    value = value.substring(50);
+                } else {
+                    value = "";
+                }
+            }
+        }
+        sb.append("</html>");
+        return sb.toString();
+    }
+
+    /**
+     * reset the renderer
+     */
+    protected void reset() {
+        setBackground(Color.WHITE);
+        setForeground(Color.BLACK);
+    }
+
+    /**
+     * render a node
+     * @param model  the model
+     * @param node the node
+     * @param isSelected true, if the current row is selected
+     */
+    protected  void renderNode(ListMergeModel<Node>.EntriesTableModel model, Node node, int row, boolean isSelected) {
+        setIcon(icon);
+        setBorder(null);
+        if (model.getListMergeModel().isFrozen()) {
+            setBackground(BGCOLOR_FROZEN);
+        } else if (isSelected) {
+            setBackground(BGCOLOR_SELECTED);
+        } else if (model.isParticipatingInCurrentComparePair()) {
+            if (model.isSamePositionInOppositeList(row)) {
+                setBackground(BGCOLOR_SAME_POSITION_IN_OPPOSITE);
+            } else if (model.isIncludedInOppositeList(row)) {
+                setBackground(BGCOLOR_IN_OPPOSITE);
+            } else {
+                setBackground(BGCOLOR_NOT_IN_OPPOSITE);
+            }
+        }
+        setText(node.getName());
+        setToolTipText(buildToolTipText(node));
+    }
+
+    /**
+     * render an empty row
+     */
+    protected void renderEmptyRow() {
+        setIcon(null);
+        setBackground(BGCOLOR_EMPTY_ROW);
+        setText("");
+    }
+
+    /**
+     * render the row id
+     * @param model  the model
+     * @param row the row index
+     * @param isSelected true, if the current row is selected
+     */
+    protected  void renderRowId( ListMergeModel<Node>.EntriesTableModel model, int row, boolean isSelected) {
+        setIcon(null);
+        setBorder(rowNumberBorder);
+        if (model.getListMergeModel().isFrozen()) {
+            setBackground(BGCOLOR_FROZEN);
+        } else if (model.isParticipatingInCurrentComparePair()) {
+            setBackground(BGCOLOR_PARTICIPAING_IN_COMPARISON);
+            setForeground(FGCOLOR_PARTICIPAING_IN_COMPARISON);
+        }
+        setText(Integer.toString(row+1));
+    }
+
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
+            int row, int column) {
+
+        Node node = (Node)value;
+        reset();
+        switch(column) {
+            case 0:
+                renderRowId(getModel(table),row, isSelected);
+                break;
+            case 1:
+                if (node == null) {
+                    renderEmptyRow();
+                } else {
+                    renderNode(getModel(table), node, row, isSelected);
+                }
+                break;
+            default:
+                // should not happen
+                throw new RuntimeException(tr("unexpected column index. Got {0}", column));
+        }
+        return this;
+    }
+
+    /**
+     * replies the model
+     * @param table  the table
+     * @return the table model
+     */
+    @SuppressWarnings("unchecked")
+    protected ListMergeModel<Node>.EntriesTableModel getModel(JTable table) {
+        return (ListMergeModel.EntriesTableModel)table.getModel();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/OperationCancelledException.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/OperationCancelledException.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/OperationCancelledException.java	(revision 1954)
@@ -0,0 +1,27 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.properties;
+
+public class OperationCancelledException extends Exception {
+
+    public OperationCancelledException() {
+        super();
+        // TODO Auto-generated constructor stub
+    }
+
+    public OperationCancelledException(String message, Throwable cause) {
+        super(message, cause);
+        // TODO Auto-generated constructor stub
+    }
+
+    public OperationCancelledException(String message) {
+        super(message);
+        // TODO Auto-generated constructor stub
+    }
+
+    public OperationCancelledException(Throwable cause) {
+        super(cause);
+        // TODO Auto-generated constructor stub
+    }
+
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMergeModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMergeModel.java	(revision 1954)
@@ -0,0 +1,597 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.properties;
+
+import static org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType.UNDECIDED;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Observable;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.CoordinateConflictResolveCommand;
+import org.openstreetmap.josm.command.DeletedStateConflictResolveCommand;
+import org.openstreetmap.josm.command.PurgePrimitivesCommand;
+import org.openstreetmap.josm.command.UndeletePrimitivesCommand;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+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.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.OptionPaneUtil;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
+import org.openstreetmap.josm.io.OsmTransferException;
+
+/**
+ * This is the model for resolving conflicts in the properties of the
+ * {@see OsmPrimitive}s. In particular, it represents conflicts in the coordiates of {@see Node}s and
+ * the deleted or visible state of {@see OsmPrimitive}s.
+ *
+ * This model is an {@see Observable}. It notifies registered {@see Observer}s whenever the
+ * internal state changes.
+ *
+ * This model also emits property changes for {@see #RESOLVED_COMPLETELY_PROP}. Property change
+ * listeners may register themselves using {@see #addPropertyChangeListener(PropertyChangeListener)}.
+ *
+ * @see Node#getCoor()
+ * @see OsmPrimitive#deleted
+ * @see OsmPrimitive#visible
+ *
+ */
+public class PropertiesMergeModel extends Observable {
+
+    static public final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely";
+
+    private OsmPrimitive my;
+
+    private LatLon myCoords;
+    private LatLon theirCoords;
+    private MergeDecisionType coordMergeDecision;
+
+    private boolean myDeletedState;
+    private boolean theirDeletedState;
+    private boolean myVisibleState;
+    private boolean theirVisibleState;
+    private MergeDecisionType deletedMergeDecision;
+    private MergeDecisionType visibleMergeDecision;
+    private final PropertyChangeSupport support;
+    private boolean resolvedCompletely;
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        support.addPropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        support.removePropertyChangeListener(listener);
+    }
+
+    public void fireCompletelyResolved() {
+        boolean oldValue = resolvedCompletely;
+        resolvedCompletely = isResolvedCompletely();
+        support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, resolvedCompletely);
+    }
+
+    public PropertiesMergeModel() {
+        coordMergeDecision = UNDECIDED;
+        deletedMergeDecision = UNDECIDED;
+        support = new PropertyChangeSupport(this);
+        resolvedCompletely = false;
+    }
+
+    /**
+     * replies true if there is a coordinate conflict and if this conflict is
+     * resolved
+     *
+     * @return true if there is a coordinate conflict and if this conflict is
+     * resolved; false, otherwise
+     */
+    public boolean isDecidedCoord() {
+        return ! coordMergeDecision.equals(UNDECIDED);
+    }
+
+    /**
+     * replies true if there is a  conflict in the deleted state and if this conflict is
+     * resolved
+     *
+     * @return true if there is a conflict in the deleted state and if this conflict is
+     * resolved; false, otherwise
+     */
+    public boolean isDecidedDeletedState() {
+        return ! deletedMergeDecision.equals(UNDECIDED);
+    }
+
+    /**
+     * replies true if there is a  conflict in the visible state and if this conflict is
+     * resolved
+     *
+     * @return true if there is a conflict in the visible state and if this conflict is
+     * resolved; false, otherwise
+     */
+    public boolean isDecidedVisibleState() {
+        return ! visibleMergeDecision.equals(UNDECIDED);
+    }
+
+    /**
+     * replies true if the current decision for the coordinate conflict is <code>decision</code>
+     *
+     * @return true if the current decision for the coordinate conflict is <code>decision</code>;
+     *  false, otherwise
+     */
+    public boolean isCoordMergeDecision(MergeDecisionType decision) {
+        return coordMergeDecision.equals(decision);
+    }
+
+    /**
+     * replies true if the current decision for the deleted state conflict is <code>decision</code>
+     *
+     * @return true if the current decision for the deleted state conflict is <code>decision</code>;
+     *  false, otherwise
+     */
+    public boolean isDeletedStateDecision(MergeDecisionType decision) {
+        return deletedMergeDecision.equals(decision);
+    }
+
+    /**
+     * replies true if the current decision for the visible state conflict is <code>decision</code>
+     *
+     * @return true if the current decision for the visible state conflict is <code>decision</code>;
+     *  false, otherwise
+     */
+    public boolean isVisibleStateDecision(MergeDecisionType decision) {
+        return visibleMergeDecision.equals(decision);
+    }
+    /**
+     * populates the model with the differences between my and their version
+     *
+     * @param my my version of the primitive
+     * @param their their version of the primitive
+     */
+    public void populate(OsmPrimitive my, OsmPrimitive their) {
+        this.my = my;
+        if (my instanceof Node) {
+            myCoords = ((Node)my).getCoor();
+            theirCoords = ((Node)their).getCoor();
+        } else {
+            myCoords = null;
+            theirCoords = null;
+        }
+
+        myDeletedState = my.deleted;
+        theirDeletedState = their.deleted;
+
+        myVisibleState = my.visible;
+        theirVisibleState = their.visible;
+
+        coordMergeDecision = UNDECIDED;
+        deletedMergeDecision = UNDECIDED;
+        visibleMergeDecision = UNDECIDED;
+        setChanged();
+        notifyObservers();
+        fireCompletelyResolved();
+    }
+
+
+    /**
+     * replies the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
+     * coordinates (i.e. because it is a {@see Way}).
+     *
+     * @return the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
+     *  coordinates (i.e. because it is a {@see Way}).
+     */
+    public LatLon getMyCoords() {
+        return myCoords;
+    }
+
+    /**
+     * replies the coordinates of their {@see OsmPrimitive}. null, if their primitive hasn't
+     * coordinates (i.e. because it is a {@see Way}).
+     *
+     * @return the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
+     * coordinates (i.e. because it is a {@see Way}).
+     */
+    public LatLon getTheirCoords() {
+        return theirCoords;
+    }
+
+    /**
+     * replies the coordinates of the merged {@see OsmPrimitive}. null, if the current primitives
+     * have no coordinates or if the conflict is yet {@see MergeDecisionType#UNDECIDED}
+     *
+     * @return the coordinates of the merged {@see OsmPrimitive}. null, if the current primitives
+     * have no coordinates or if the conflict is yet {@see MergeDecisionType#UNDECIDED}
+     */
+    public LatLon getMergedCoords() {
+        switch(coordMergeDecision) {
+        case KEEP_MINE: return myCoords;
+        case KEEP_THEIR: return theirCoords;
+        case UNDECIDED: return null;
+        }
+        // should not happen
+        return null;
+    }
+
+    /**
+     * decides a conflict between my and their coordinates
+     *
+     * @param decision the decision
+     */
+    public void decideCoordsConflict(MergeDecisionType decision) {
+        coordMergeDecision = decision;
+        setChanged();
+        notifyObservers();
+        fireCompletelyResolved();
+    }
+
+    /**
+     * replies my deleted state,
+     * @return
+     */
+    public Boolean getMyDeletedState() {
+        return myDeletedState;
+    }
+
+    public  Boolean getTheirDeletedState() {
+        return theirDeletedState;
+    }
+
+    public Boolean getMergedDeletedState() {
+        switch(deletedMergeDecision) {
+        case KEEP_MINE: return myDeletedState;
+        case KEEP_THEIR: return theirDeletedState;
+        case UNDECIDED: return null;
+        }
+        // should not happen
+        return null;
+    }
+
+
+    /**
+     * replies my visible state,
+     * @return my visible state
+     */
+    public Boolean getMyVisibleState() {
+        return myVisibleState;
+    }
+
+    /**
+     * replies their visible state,
+     * @return their visible state
+     */
+    public  Boolean getTheirVisibleState() {
+        return theirVisibleState;
+    }
+
+    /**
+     * replies the merged visible state; null, if the merge decision is
+     * {@see MergeDecisionType#UNDECIDED}.
+     *
+     * @return the merged visible state
+     */
+    public Boolean getMergedVisibleState() {
+        switch(visibleMergeDecision) {
+        case KEEP_MINE: return myVisibleState;
+        case KEEP_THEIR: return theirVisibleState;
+        case UNDECIDED: return null;
+        }
+        // should not happen
+        return null;
+    }
+
+    /**
+     * decides the conflict between two deleted states
+     * @param decision the decision (must not be null)
+     *
+     * @throws IllegalArgumentException thrown, if decision is null
+     */
+    public void decideDeletedStateConflict(MergeDecisionType decision) throws IllegalArgumentException{
+        if (decision == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "decision"));
+        this.deletedMergeDecision = decision;
+        setChanged();
+        notifyObservers();
+        fireCompletelyResolved();
+    }
+
+    /**
+     * decides the conflict between two visible states
+     * @param decision the decision (must not be null)
+     *
+     * @throws IllegalArgumentException thrown, if decision is null
+     */
+    public void decideVisibleStateConflict(MergeDecisionType decision) throws IllegalArgumentException {
+        if (decision == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "decision"));
+        this.visibleMergeDecision = decision;
+        setChanged();
+        notifyObservers();
+        fireCompletelyResolved();
+    }
+
+    /**
+     * replies true if my and their primitive have a conflict between
+     * their coordinate values
+     *
+     * @return true if my and their primitive have a conflict between
+     * their coordinate values; false otherwise
+     */
+    public boolean hasCoordConflict() {
+        if (myCoords == null && theirCoords != null) return true;
+        if (myCoords != null && theirCoords == null) return true;
+        if (myCoords == null && theirCoords == null) return false;
+        return !myCoords.equals(theirCoords);
+    }
+
+    /**
+     * replies true if my and their primitive have a conflict between
+     * their deleted states
+     *
+     * @return true if my and their primitive have a conflict between
+     * their deleted states
+     */
+    public boolean hasDeletedStateConflict() {
+        return myDeletedState != theirDeletedState;
+    }
+
+    /**
+     * replies true if my and their primitive have a conflict between
+     * their visible states
+     *
+     * @return true if my and their primitive have a conflict between
+     * their visible states
+     */
+    public boolean hasVisibleStateConflict() {
+        return myVisibleState != theirVisibleState;
+    }
+
+    /**
+     * replies true if all conflict in this model are resolved
+     *
+     * @return true if all conflict in this model are resolved; false otherwise
+     */
+    public boolean isResolvedCompletely() {
+        boolean ret = true;
+        if (hasCoordConflict()) {
+            ret = ret && ! coordMergeDecision.equals(UNDECIDED);
+        }
+        if (hasDeletedStateConflict()) {
+            ret = ret && ! deletedMergeDecision.equals(UNDECIDED);
+        }
+        if (hasVisibleStateConflict()) {
+            ret = ret && ! visibleMergeDecision.equals(UNDECIDED);
+        }
+        return ret;
+    }
+
+    /**
+     * builds the command(s) to apply the conflict resolutions to my primitive
+     *
+     * @param my  my primitive
+     * @param their their primitive
+     * @return the list of commands
+     */
+    public List<Command> buildResolveCommand(OsmPrimitive my, OsmPrimitive their) throws OperationCancelledException{
+        ArrayList<Command> cmds = new ArrayList<Command>();
+        if (hasVisibleStateConflict() && isDecidedVisibleState()) {
+            if (isVisibleStateDecision(MergeDecisionType.KEEP_MINE)) {
+                try {
+                    UndeletePrimitivesCommand cmd = createUndeletePrimitiveCommand(my);
+                    if (cmd == null)
+                        throw new OperationCancelledException();
+                    cmds.add(cmd);
+                } catch(OsmTransferException e) {
+                    handleExceptionWhileBuildingCommand(e);
+                    throw new OperationCancelledException(e);
+                }
+            } else if (isVisibleStateDecision(MergeDecisionType.KEEP_THEIR)) {
+                cmds.add(new PurgePrimitivesCommand(my));
+            }
+        }
+        if (hasCoordConflict() && isDecidedCoord()) {
+            cmds.add(new CoordinateConflictResolveCommand((Node)my, (Node)their, coordMergeDecision));
+        }
+        if (hasDeletedStateConflict() && isDecidedDeletedState()) {
+            cmds.add(new DeletedStateConflictResolveCommand(my, their, deletedMergeDecision));
+        }
+        return cmds;
+    }
+
+    public OsmPrimitive getMyPrimitive() {
+        return my;
+    }
+
+    /**
+     *
+     * @param id
+     */
+    protected void handleExceptionWhileBuildingCommand(Exception e) {
+        e.printStackTrace();
+        String msg = e.getMessage() != null ? e.getMessage() : e.toString();
+        msg = msg.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+        OptionPaneUtil.showMessageDialog(
+                Main.parent,
+                tr("<html>An error occurred while communicating with the server<br>"
+                        + "Details: {0}</html>",
+                        msg
+                ),
+                tr("Communication with server failed"),
+                JOptionPane.ERROR_MESSAGE
+        );
+    }
+
+    /**
+     * User has decided to keep his local version of a primitive which had been deleted
+     * on the server
+     *
+     * @param id the primitive id
+     */
+    protected UndeletePrimitivesCommand createUndeletePrimitiveCommand(OsmPrimitive my) throws OsmTransferException {
+        if (my instanceof Node)
+            return createUndeleteNodeCommand((Node)my);
+        else if (my instanceof Way)
+            return createUndeleteWayCommand((Way)my);
+        else if (my instanceof Relation)
+            return createUndeleteRelationCommand((Relation)my);
+        return null;
+    }
+    /**
+     * Undelete a node which is already deleted on the server. The API
+     * doesn't offer a call for "undeleting" a node. We therefore create
+     * a clone of the node which we flag as new. On the next upload the
+     * server will assign the node a new id.
+     *
+     * @param node the node to undelete
+     */
+    protected UndeletePrimitivesCommand  createUndeleteNodeCommand(Node node) {
+        return new UndeletePrimitivesCommand(node);
+    }
+
+    /**
+     * displays a confirmation message. The user has to confirm that additional dependent
+     * nodes should be undeleted too.
+     *
+     * @param way  the way
+     * @param dependent a list of dependent nodes which have to be undelete too
+     * @return true, if the user confirms; false, otherwise
+     */
+    protected boolean confirmUndeleteDependentPrimitives(Way way, ArrayList<OsmPrimitive> dependent) {
+        String [] options = {
+                tr("Yes, undelete them too"),
+                tr("No, cancel operation")
+        };
+        int ret = OptionPaneUtil.showOptionDialog(
+                Main.parent,
+                tr("<html>There are {0} additional nodes used by way {1}<br>"
+                        + "which are deleted on the server.<br>"
+                        + "<br>"
+                        + "Do you want to undelete these nodes too?</html>",
+                        Long.toString(dependent.size()), Long.toString(way.id)),
+                        tr("Undelete additional nodes?"),
+                        JOptionPane.YES_NO_OPTION,
+                        JOptionPane.QUESTION_MESSAGE,
+                        options,
+                        options[0]
+        );
+
+        switch(ret) {
+        case JOptionPane.CLOSED_OPTION: return false;
+        case JOptionPane.YES_OPTION: return true;
+        case JOptionPane.NO_OPTION: return false;
+        }
+        return false;
+
+    }
+
+    protected boolean confirmUndeleteDependentPrimitives(Relation r, ArrayList<OsmPrimitive> dependent) {
+        String [] options = {
+                tr("Yes, undelete them too"),
+                tr("No, cancel operation")
+        };
+        int ret = OptionPaneUtil.showOptionDialog(
+                Main.parent,
+                tr("<html>There are {0} additional primitives referred to by relation {1}<br>"
+                        + "which are deleted on the server.<br>"
+                        + "<br>"
+                        + "Do you want to undelete them too?</html>",
+                        Long.toString(dependent.size()), Long.toString(r.id)),
+                        tr("Undelete dependent primitives?"),
+                        JOptionPane.YES_NO_OPTION,
+                        JOptionPane.QUESTION_MESSAGE,
+                        options,
+                        options[0]
+        );
+
+        switch(ret) {
+        case JOptionPane.CLOSED_OPTION: return false;
+        case JOptionPane.YES_OPTION: return true;
+        case JOptionPane.NO_OPTION: return false;
+        }
+        return false;
+
+    }
+
+    /**
+     * Creates the undelete command for a way which is already deleted on the server.
+     *
+     * This method also checks whether there are additional nodes referred to by
+     * this way which are deleted on the server too.
+     *
+     * @param way the way to undelete
+     * @return the undelete command
+     * @see #createUndeleteNodeCommand(Node)
+     */
+    protected UndeletePrimitivesCommand createUndeleteWayCommand(final Way way) throws OsmTransferException {
+
+        HashMap<Long,OsmPrimitive> candidates = new HashMap<Long,OsmPrimitive>();
+        for (Node n : way.getNodes()) {
+            if (n.id > 0 && ! candidates.values().contains(n)) {
+                candidates.put(n.id, n);
+            }
+        }
+        MultiFetchServerObjectReader reader = new MultiFetchServerObjectReader();
+        reader.append(candidates.values());
+        DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
+
+        ArrayList<OsmPrimitive> toDelete = new ArrayList<OsmPrimitive>();
+        for (OsmPrimitive their : ds.allPrimitives()) {
+            if (candidates.keySet().contains(their.id) && ! their.visible) {
+                toDelete.add(candidates.get(their.id));
+            }
+        }
+        if (!toDelete.isEmpty()) {
+            if (! confirmUndeleteDependentPrimitives(way, toDelete))
+                // FIXME: throw exception ?
+                return null;
+        }
+        toDelete.add(way);
+        return new UndeletePrimitivesCommand(toDelete);
+    }
+
+    /**
+     * Creates an undelete command for a relation which is already deleted on the server.
+     *
+     * This method  checks whether there are additional primitives referred to by
+     * this relation which are already deleted on the server.
+     *
+     * @param r the relation
+     * @return the undelete command
+     * @see #createUndeleteNodeCommand(Node)
+     */
+    protected UndeletePrimitivesCommand createUndeleteRelationCommand(final Relation r) throws OsmTransferException {
+
+        HashMap<Long,OsmPrimitive> candidates = new HashMap<Long, OsmPrimitive>();
+        for (RelationMember m : r.getMembers()) {
+            if (m.getMember().id > 0 && !candidates.values().contains(m.getMember())) {
+                candidates.put(m.getMember().id, m.getMember());
+            }
+        }
+
+        MultiFetchServerObjectReader reader = new MultiFetchServerObjectReader();
+        reader.append(candidates.values());
+        DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
+
+        ArrayList<OsmPrimitive> toDelete = new ArrayList<OsmPrimitive>();
+        for (OsmPrimitive their : ds.allPrimitives()) {
+            if (candidates.keySet().contains(their.id) && ! their.visible) {
+                toDelete.add(candidates.get(their.id));
+            }
+        }
+        if (!toDelete.isEmpty()) {
+            if (! confirmUndeleteDependentPrimitives(r, toDelete))
+                // FIXME: throw exception ?
+                return null;
+        }
+        toDelete.add(r);
+        return new UndeletePrimitivesCommand(toDelete);
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMerger.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMerger.java	(revision 1954)
@@ -0,0 +1,657 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.text.DecimalFormat;
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.gui.OptionPaneUtil;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This class represents a UI component for resolving conflicts in some properties
+ * of {@see OsmPrimitive}.
+ * 
+ */
+public class PropertiesMerger extends JPanel implements Observer {
+    private static DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000");
+
+    public final static Color BGCOLOR_NO_CONFLICT = new Color(234,234,234);
+    public final static Color BGCOLOR_UNDECIDED = new Color(255,197,197);
+    public final static Color BGCOLOR_DECIDED = new Color(217,255,217);
+
+    private  JLabel lblMyVersion;
+    private  JLabel lblMergedVersion;
+    private  JLabel lblTheirVersion;
+
+    private JLabel lblMyCoordinates;
+    private JLabel lblMergedCoordinates;
+    private JLabel lblTheirCoordinates;
+
+    private JLabel lblMyDeletedState;
+    private JLabel lblMergedDeletedState;
+    private JLabel lblTheirDeletedState;
+
+    private JLabel lblMyVisibleState;
+    private JLabel lblMergedVisibleState;
+    private JLabel lblTheirVisibleState;
+
+    private final PropertiesMergeModel model;
+
+    protected JLabel buildValueLabel(String name) {
+        JLabel lbl = new JLabel();
+        lbl.setName(name);
+        lbl.setHorizontalAlignment(JLabel.CENTER);
+        lbl.setOpaque(true);
+        lbl.setBorder(BorderFactory.createLoweredBevelBorder());
+        return lbl;
+    }
+
+    protected void buildHeaderRow() {
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 1;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(10,0,10,0);
+        lblMyVersion = new JLabel(tr("My version"));
+        lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset"));
+        add(lblMyVersion, gc);
+
+        gc.gridx = 3;
+        gc.gridy = 0;
+        lblMergedVersion = new JLabel(tr("Merged version"));
+        lblMergedVersion.setToolTipText(tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied."));
+        add(lblMergedVersion, gc);
+
+        gc.gridx = 5;
+        gc.gridy = 0;
+        lblTheirVersion = new JLabel(tr("Their version"));
+        lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset"));
+        add(lblTheirVersion, gc);
+    }
+
+    protected void buildCoordinateConflictRows() {
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,5,0,5);
+        add(new JLabel(tr("Coordinates:")), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMyCoordinates = buildValueLabel("label.mycoordinates"), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction();
+        model.addObserver(actKeepMyCoordinates);
+        JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates);
+        btnKeepMyCoordinates.setName("button.keepmycoordinates");
+        add(btnKeepMyCoordinates, gc);
+
+
+        gc.gridx = 3;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction();
+        model.addObserver(actKeepTheirCoordinates);
+        JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates);
+        add(btnKeepTheirCoordinates, gc);
+
+        gc.gridx = 5;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblTheirCoordinates = buildValueLabel("label.theircoordinates"), gc);
+
+        // ---------------------------------------------------
+        gc.gridx = 3;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,5,20,5);
+        UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction();
+        model.addObserver(actUndecideCoordinates);
+        JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates);
+        add(btnUndecideCoordinates, gc);
+    }
+
+    protected void buildDeletedStateConflictRows() {
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 3;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,5,0,5);
+        add(new JLabel(tr("Deleted State:")), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 3;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMyDeletedState = buildValueLabel("label.mydeletedstate"), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 3;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction();
+        model.addObserver(actKeepMyDeletedState);
+        JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState);
+        btnKeepMyDeletedState.setName("button.keepmydeletedstate");
+        add(btnKeepMyDeletedState, gc);
+
+
+        gc.gridx = 3;
+        gc.gridy = 3;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 3;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction();
+        model.addObserver(actKeepTheirDeletedState);
+        JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState);
+        btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate");
+        add(btnKeepTheirDeletedState, gc);
+
+        gc.gridx = 5;
+        gc.gridy = 3;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"), gc);
+
+        // ---------------------------------------------------
+        gc.gridx = 3;
+        gc.gridy = 4;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction();
+        model.addObserver(actUndecideDeletedState);
+        JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState);
+        btnUndecideDeletedState.setName("button.undecidedeletedstate");
+        add(btnUndecideDeletedState, gc);
+    }
+
+    protected void buildVisibleStateRows() {
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 5;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,5,0,5);
+        add(new JLabel(tr("Visible State:")), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 5;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMyVisibleState = buildValueLabel("label.myvisiblestate"), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 5;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepMyVisibleStateAction actKeepMyVisibleState = new KeepMyVisibleStateAction();
+        model.addObserver(actKeepMyVisibleState);
+        JButton btnKeepMyVisibleState = new JButton(actKeepMyVisibleState);
+        btnKeepMyVisibleState.setName("button.keepmyvisiblestate");
+        add(btnKeepMyVisibleState, gc);
+
+        gc.gridx = 3;
+        gc.gridy = 5;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblMergedVisibleState = buildValueLabel("label.mergedvisiblestate"), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 5;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepTheirVisibleStateAction actKeepTheirVisibleState = new KeepTheirVisibleStateAction();
+        model.addObserver(actKeepTheirVisibleState);
+        JButton btnKeepTheirVisibleState = new JButton(actKeepTheirVisibleState);
+        btnKeepTheirVisibleState.setName("button.keeptheirvisiblestate");
+        add(btnKeepTheirVisibleState, gc);
+
+        gc.gridx = 5;
+        gc.gridy = 5;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        add(lblTheirVisibleState = buildValueLabel("label.theirvisiblestate"), gc);
+
+        // ---------------------------------------------------
+        gc.gridx = 3;
+        gc.gridy = 6;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        UndecideVisibleStateConflictAction actUndecideVisibleState = new UndecideVisibleStateConflictAction();
+        model.addObserver(actUndecideVisibleState);
+        JButton btnUndecideVisibleState = new JButton(actUndecideVisibleState);
+        btnUndecideVisibleState.setName("button.undecidevisiblestate");
+        add(btnUndecideVisibleState, gc);
+    }
+
+    protected void build() {
+        setLayout(new GridBagLayout());
+        buildHeaderRow();
+        buildCoordinateConflictRows();
+        buildDeletedStateConflictRows();
+        buildVisibleStateRows();
+    }
+
+    public PropertiesMerger() {
+        model = new PropertiesMergeModel();
+        model.addObserver(this);
+        build();
+    }
+
+    public String coordToString(LatLon coord) {
+        if (coord == null)
+            return tr("(none)");
+        StringBuilder sb = new StringBuilder();
+        sb.append("(")
+        .append(COORD_FORMATTER.format(coord.lat()))
+        .append(",")
+        .append(COORD_FORMATTER.format(coord.lon()))
+        .append(")");
+        return sb.toString();
+    }
+
+    public String deletedStateToString(Boolean deleted) {
+        if (deleted == null)
+            return tr("(none)");
+        if (deleted)
+            return tr("deleted");
+        else
+            return tr("not deleted");
+    }
+
+    public String visibleStateToString(Boolean visible) {
+        if (visible == null)
+            return tr("(none)");
+        if (visible)
+            return tr("visible (on the server)");
+        else
+            return tr("not visible (on the server)");
+    }
+
+    public String visibleStateToStringMerged(Boolean visible) {
+        if (visible == null)
+            return tr("(none)");
+        if (visible)
+            return tr("Keep a clone of the local version");
+        else
+            return tr("Physically delete from local dataset");
+    }
+
+    protected void updateCoordinates() {
+        lblMyCoordinates.setText(coordToString(model.getMyCoords()));
+        lblMergedCoordinates.setText(coordToString(model.getMergedCoords()));
+        lblTheirCoordinates.setText(coordToString(model.getTheirCoords()));
+        if (! model.hasCoordConflict()) {
+            lblMyCoordinates.setBackground(BGCOLOR_NO_CONFLICT);
+            lblMergedCoordinates.setBackground(BGCOLOR_NO_CONFLICT);
+            lblTheirCoordinates.setBackground(BGCOLOR_NO_CONFLICT);
+        } else {
+            if (!model.isDecidedCoord()) {
+                lblMyCoordinates.setBackground(BGCOLOR_UNDECIDED);
+                lblMergedCoordinates.setBackground(BGCOLOR_NO_CONFLICT);
+                lblTheirCoordinates.setBackground(BGCOLOR_UNDECIDED);
+            } else {
+                lblMyCoordinates.setBackground(
+                        model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+                lblMergedCoordinates.setBackground(BGCOLOR_DECIDED);
+                lblTheirCoordinates.setBackground(
+                        model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+            }
+        }
+    }
+
+    protected void updateDeletedState() {
+        lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState()));
+        lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState()));
+        lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState()));
+
+        if (! model.hasDeletedStateConflict()) {
+            lblMyDeletedState.setBackground(BGCOLOR_NO_CONFLICT);
+            lblMergedDeletedState.setBackground(BGCOLOR_NO_CONFLICT);
+            lblTheirDeletedState.setBackground(BGCOLOR_NO_CONFLICT);
+        } else {
+            if (!model.isDecidedDeletedState()) {
+                lblMyDeletedState.setBackground(BGCOLOR_UNDECIDED);
+                lblMergedDeletedState.setBackground(BGCOLOR_NO_CONFLICT);
+                lblTheirDeletedState.setBackground(BGCOLOR_UNDECIDED);
+            } else {
+                lblMyDeletedState.setBackground(
+                        model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+                lblMergedDeletedState.setBackground(BGCOLOR_DECIDED);
+                lblTheirDeletedState.setBackground(
+                        model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+            }
+        }
+    }
+
+    protected void updateVisibleState() {
+        lblMyVisibleState.setText(visibleStateToString(model.getMyVisibleState()));
+        lblMergedVisibleState.setText(visibleStateToStringMerged(model.getMergedVisibleState()));
+        lblTheirVisibleState.setText(visibleStateToString(model.getTheirVisibleState()));
+
+        if (! model.hasVisibleStateConflict()) {
+            lblMyVisibleState.setBackground(BGCOLOR_NO_CONFLICT);
+            lblMergedVisibleState.setBackground(BGCOLOR_NO_CONFLICT);
+            lblTheirVisibleState.setBackground(BGCOLOR_NO_CONFLICT);
+        } else {
+            if (!model.isDecidedVisibleState()) {
+                lblMyVisibleState.setBackground(BGCOLOR_UNDECIDED);
+                lblMergedVisibleState.setBackground(BGCOLOR_NO_CONFLICT);
+                lblTheirVisibleState.setBackground(BGCOLOR_UNDECIDED);
+            } else {
+                lblMyVisibleState.setBackground(
+                        model.isVisibleStateDecision(MergeDecisionType.KEEP_MINE)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+                lblMergedVisibleState.setBackground(BGCOLOR_DECIDED);
+                lblTheirVisibleState.setBackground(
+                        model.isVisibleStateDecision(MergeDecisionType.KEEP_THEIR)
+                        ? BGCOLOR_DECIDED : BGCOLOR_NO_CONFLICT
+                );
+            }
+        }
+    }
+
+    public void update(Observable o, Object arg) {
+        updateCoordinates();
+        updateDeletedState();
+        updateVisibleState();
+    }
+
+    public PropertiesMergeModel getModel() {
+        return model;
+    }
+
+    class KeepMyCoordinatesAction extends AbstractAction implements Observer {
+        public KeepMyCoordinatesAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordiates"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord());
+        }
+    }
+
+    class KeepTheirCoordinatesAction extends AbstractAction implements Observer {
+        public KeepTheirCoordinatesAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordiates"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord());
+        }
+    }
+
+    class UndecideCoordinateConflictAction extends AbstractAction implements Observer {
+        public UndecideCoordinateConflictAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasCoordConflict() && model.isDecidedCoord());
+        }
+    }
+
+    class KeepMyDeletedStateAction extends AbstractAction implements Observer {
+        public KeepMyDeletedStateAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState());
+        }
+    }
+
+    class KeepTheirDeletedStateAction extends AbstractAction implements Observer {
+        public KeepTheirDeletedStateAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState());
+        }
+    }
+
+    class UndecideDeletedStateConflictAction extends AbstractAction implements Observer {
+        public UndecideDeletedStateConflictAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState());
+        }
+    }
+
+    class KeepMyVisibleStateAction extends AbstractAction implements Observer {
+        public KeepMyVisibleStateAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep my visible state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            if (confirmKeepMine()) {
+                model.decideVisibleStateConflict(MergeDecisionType.KEEP_MINE);
+            }
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasVisibleStateConflict() && ! model.isDecidedVisibleState());
+        }
+
+        protected boolean confirmKeepMine() {
+            String [] options = {
+                    tr("Yes, reset the id"),
+                    tr("No, abort")
+            };
+            int ret = OptionPaneUtil.showOptionDialog(
+                    null,
+                    tr("<html>To keep your local version, JOSM<br>"
+                            + "has to reset the id of {0} {1} to 0.<br>"
+                            + "On the next upload the server will assign<br>"
+                            + "it a new id.<br>"
+                            + "Do yo agree?</html>",
+                            OsmPrimitiveType.from(model.getMyPrimitive()).getLocalizedDisplayNamePlural(),
+                            model.getMyPrimitive().id
+                    ),
+                    tr("Reset id to 0"),
+                    JOptionPane.YES_NO_OPTION,
+                    JOptionPane.QUESTION_MESSAGE,
+                    options,
+                    options[1]
+            );
+            return ret == JOptionPane.YES_OPTION;
+        }
+    }
+
+    class KeepTheirVisibleStateAction extends AbstractAction implements Observer {
+        public KeepTheirVisibleStateAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep their visible state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            if (confirmKeepTheir()){
+                model.decideVisibleStateConflict(MergeDecisionType.KEEP_THEIR);
+            }
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasVisibleStateConflict() && ! model.isDecidedVisibleState());
+        }
+
+        protected boolean confirmKeepTheir() {
+            String [] options = {
+                    tr("Yes, purge it"),
+                    tr("No, abort")
+            };
+            int ret = OptionPaneUtil.showOptionDialog(
+                    null,
+                    tr("<html>JOSM will have to remove your local primitive with id {0}<br>"
+                            + "from the dataset.<br>"
+                            + "Do you agree?</html>",
+                            model.getMyPrimitive().id
+                    ),
+                    tr("Remove from dataset"),
+                    JOptionPane.YES_NO_OPTION,
+                    JOptionPane.QUESTION_MESSAGE,
+                    options,
+                    options[1]
+            );
+            return ret == JOptionPane.YES_OPTION;
+        }
+    }
+
+    class UndecideVisibleStateConflictAction extends AbstractAction implements Observer {
+        public UndecideVisibleStateConflictAction() {
+            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between visible state"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            model.decideVisibleStateConflict(MergeDecisionType.UNDECIDED);
+        }
+
+        public void update(Observable o, Object arg) {
+            setEnabled(model.hasVisibleStateConflict() && model.isDecidedVisibleState());
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListColumnModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListColumnModel.java	(revision 1954)
@@ -0,0 +1,44 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.relation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableColumn;
+
+public class RelationMemberListColumnModel extends DefaultTableColumnModel{
+
+    protected void createColumns() {
+        TableColumn col = null;
+        RelationMemberTableCellRenderer renderer = new RelationMemberTableCellRenderer();
+
+        // column 0 - Role
+        col = new TableColumn(0);
+        col.setHeaderValue("");
+        col.setResizable(true);
+        col.setWidth(20);
+        col.setPreferredWidth(20);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+
+        // column 1 - Role
+        col = new TableColumn(1);
+        col.setHeaderValue(tr("Role"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setMaxWidth(100);
+        col.setCellEditor(new RelationMemberTableCellEditor());
+        addColumn(col);
+
+        // column 2 - Primitive
+        col = new TableColumn(2);
+        col.setHeaderValue(tr("Primitive"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+    }
+
+    public RelationMemberListColumnModel() {
+        createColumns();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java	(revision 1954)
@@ -0,0 +1,121 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.relation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.logging.Logger;
+
+import javax.swing.table.DefaultTableModel;
+
+import org.openstreetmap.josm.command.RelationMemberConflictResolverCommand;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.ListRole;
+/**
+ * The model for merging two lists of relation members
+ *
+ *
+ */
+public class RelationMemberListMergeModel extends ListMergeModel<RelationMember>{
+
+    private static final Logger logger = Logger.getLogger(RelationMemberListMergeModel.class.getName());
+
+    @Override
+    public boolean isEqualEntry(RelationMember e1, RelationMember e2) {
+        boolean ret = e1.getRole().equals(e2.getRole());
+        if (e1.getMember().id > 0 ) {
+            ret = ret && (e1.getMember().id == e2.getMember().id);
+        } else {
+            ret = ret && (e1 == e2);
+        }
+        return ret;
+    }
+
+    @Override
+    protected void buildMergedEntriesTableModel() {
+        // the table model for merged entries is different because it supports
+        // editing cells in the first column
+        //
+        mergedEntriesTableModel = this.new EntriesTableModel(ListRole.MERGED_ENTRIES) {
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                switch(column) {
+                case 1: return true;
+                default: return false;
+                }
+            }
+        };
+    }
+
+    @Override
+    protected void setValueAt(DefaultTableModel model, Object value, int row, int col) {
+        if (model == getMergedTableModel() && col == 1) {
+            RelationMember member = getMergedEntries().get(row);
+            member.role = (String)value;
+            fireModelDataChanged();
+        }
+    }
+
+    /**
+     * populates the model with the relation members in relation my and their
+     *
+     * @param my my relation. Must not be null.
+     * @param their their relation. Must not be null.
+     *
+     * @throws IllegalArgumentException if my is null
+     * @throws IllegalArgumentException if their is null
+     */
+    public void populate(Relation my, Relation their) {
+        if (my == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "my"));
+        if (their == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "their"));
+
+        getMergedEntries().clear();
+        getMyEntries().clear();
+        getTheirEntries().clear();
+
+        for (RelationMember n : my.getMembers()) {
+            getMyEntries().add(n);
+        }
+        for (RelationMember n : their.getMembers()) {
+            getTheirEntries().add(n);
+        }
+        if (myAndTheirEntriesEqual()) {
+            for (RelationMember m : getMyEntries()) {
+                getMergedEntries().add(cloneEntryForMergedList(m));
+            }
+            setFrozen(true);
+        } else {
+            setFrozen(false);
+        }
+
+        fireModelDataChanged();
+    }
+
+    @Override
+    protected RelationMember cloneEntryForMergedList(RelationMember entry) {
+        return new RelationMember(entry);
+    }
+
+    /**
+     * Builds the command to resolve conflicts in the node list of a way
+     *
+     * @param my  my relation. Must not be null.
+     * @param their  their relation. Must not be null
+     * @return the command
+     * @exception IllegalArgumentException thrown, if my is null
+     * @exception IllegalArgumentException thrown, if their is null
+     * @exception IllegalStateException thrown, if the merge is not yet frozen
+     */
+    public RelationMemberConflictResolverCommand buildResolveCommand(Relation my, Relation their) {
+        if (my == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "my"));
+        if (their == null)
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "their"));
+        if (! isFrozen())
+            throw new IllegalArgumentException(tr("merged nodes not frozen yet. Can't build resolution command"));
+        return new RelationMemberConflictResolverCommand(my, their, getMergedEntries());
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java	(revision 1954)
@@ -0,0 +1,64 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.relation;
+
+import java.util.logging.Logger;
+
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.conflict.pair.ListMerger;
+
+/**
+ * A UI component for resolving conflicts in the member lists of two {@see Relation}
+ */
+public class RelationMemberMerger extends ListMerger<RelationMember> {
+    private static final Logger logger = Logger.getLogger(RelationMemberMerger.class.getName());
+
+    @Override
+    protected JScrollPane buildMyElementsTable() {
+        myEntriesTable  = new JTable(
+                model.getMyTableModel(),
+                new RelationMemberListColumnModel(),
+                model.getMySelectionModel()
+        );
+        myEntriesTable.setName("table.mynodes");
+        myEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(myEntriesTable);
+    }
+
+    @Override
+    protected JScrollPane buildMergedElementsTable() {
+        mergedEntriesTable  = new JTable(
+                model.getMergedTableModel(),
+                new RelationMemberListColumnModel(),
+                model.getMergedSelectionModel()
+        );
+        mergedEntriesTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+        mergedEntriesTable.setName("table.mergednodes");
+        mergedEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(mergedEntriesTable);
+    }
+
+    @Override
+    protected JScrollPane buildTheirElementsTable() {
+        theirEntriesTable  = new JTable(
+                model.getTheirTableModel(),
+                new RelationMemberListColumnModel(),
+                model.getTheirSelectionModel()
+        );
+        theirEntriesTable.setName("table.theirnodes");
+        theirEntriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        return embeddInScrollPane(theirEntriesTable);
+    }
+
+    public void populate(Relation my, Relation their) {
+        RelationMemberListMergeModel model = (RelationMemberListMergeModel)getModel();
+        model.populate(my,their);
+    }
+
+    public RelationMemberMerger() {
+        super(new RelationMemberListMergeModel());
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellEditor.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellEditor.java	(revision 1954)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.relation;
+
+import java.awt.Component;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+
+import javax.swing.AbstractCellEditor;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.table.TableCellEditor;
+
+import org.openstreetmap.josm.data.osm.RelationMember;
+
+/**
+ * {@see TableCellEditor} for the the role column in a table for {@see RelationMember}s.
+ *
+ */
+public class RelationMemberTableCellEditor extends AbstractCellEditor implements TableCellEditor{
+
+
+    private final JTextField editor;
+
+    public RelationMemberTableCellEditor() {
+        editor = new JTextField();
+        editor.addFocusListener(
+                new FocusAdapter() {
+                    @Override
+                    public void focusGained(FocusEvent arg0) {
+                        editor.selectAll();
+                    }
+                }
+        );
+    }
+
+    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+        RelationMember member = (RelationMember)value;
+
+        editor.setText(member.getRole());
+        editor.selectAll();
+        return editor;
+    }
+
+
+    public Object getCellEditorValue() {
+        return editor.getText();
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,230 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.relation;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.border.Border;
+import javax.swing.table.TableCellRenderer;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This is the {@see TableCellRenderer} used in the tables of {@see RelationMemberMerger}.
+ *
+ */
+public  class RelationMemberTableCellRenderer extends JLabel implements TableCellRenderer {
+    private final static DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000");
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+    public final static Color BGCOLOR_EMPTY_ROW = new Color(234,234,234);
+
+    public final static Color BGCOLOR_NOT_IN_OPPOSITE = new Color(255,197,197);
+    public final static Color BGCOLOR_IN_OPPOSITE = new Color(255,234,213);
+    public final static Color BGCOLOR_SAME_POSITION_IN_OPPOSITE = new Color(217,255,217);
+
+    public final static Color BGCOLOR_PARTICIPAING_IN_COMPARISON = Color.BLACK;
+    public final static Color FGCOLOR_PARTICIPAING_IN_COMPARISON = Color.WHITE;
+
+    public final static Color BGCOLOR_FROZEN = new Color(234,234,234);
+
+    private ImageIcon nodeIcon;
+    private ImageIcon wayIcon;
+    private ImageIcon relationIcon;
+    private  Border rowNumberBorder = null;
+
+    /**
+     * Load the image icon for an OSM primitive of type node
+     *
+     * @return the icon; null, if not found
+     */
+    protected void loadIcons() {
+        nodeIcon = ImageProvider.get("data", "node");
+        wayIcon = ImageProvider.get("data", "way");
+        relationIcon = ImageProvider.get("data", "relation");
+    }
+
+    /**
+     * constructor
+     */
+    public RelationMemberTableCellRenderer() {
+        setIcon(null);
+        setOpaque(true);
+        loadIcons();
+        rowNumberBorder = BorderFactory.createEmptyBorder(0,4,0,0);
+    }
+
+
+    public String buildToolTipText(OsmPrimitive primitive) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html>");
+        sb.append("<strong>id</strong>=")
+        .append(primitive.id)
+        .append("<br>");
+        ArrayList<String> keyList = new ArrayList<String>(primitive.keySet());
+        Collections.sort(keyList);
+        for (int i = 0; i < keyList.size(); i++) {
+            if (i > 0) {
+                sb.append("<br>");
+            }
+            String key = keyList.get(i);
+            sb.append("<strong>")
+            .append(key)
+            .append("</strong>")
+            .append("=");
+            String value = primitive.get(key);
+            while(value.length() != 0) {
+                sb.append(value.substring(0,Math.min(50, value.length())));
+                if (value.length() > 50) {
+                    sb.append("<br>");
+                    value = value.substring(50);
+                } else {
+                    value = "";
+                }
+            }
+        }
+        sb.append("</html>");
+        return sb.toString();
+    }
+
+    /**
+     * reset the renderer
+     */
+    protected void reset() {
+        setBackground(Color.WHITE);
+        setForeground(Color.BLACK);
+        setBorder(null);
+        setIcon(null);
+        setToolTipText(null);
+    }
+
+
+    protected void renderBackground(ListMergeModel<Node>.EntriesTableModel model, RelationMember member, int row, int col, boolean isSelected) {
+        Color bgc = Color.WHITE;
+        if (col == 0) {
+            if (model.getListMergeModel().isFrozen()) {
+                bgc = BGCOLOR_FROZEN;
+            } else if (model.isParticipatingInCurrentComparePair()) {
+                bgc = BGCOLOR_PARTICIPAING_IN_COMPARISON;
+            } else if (isSelected) {
+                bgc = BGCOLOR_SELECTED;
+            }
+        } else {
+            if (model.getListMergeModel().isFrozen()) {
+                bgc = BGCOLOR_FROZEN;
+            } else if (member == null) {
+                bgc = BGCOLOR_EMPTY_ROW;
+            } else if (isSelected) {
+                bgc = BGCOLOR_SELECTED;
+            } else {
+                if (model.isParticipatingInCurrentComparePair()) {
+                    if (model.isSamePositionInOppositeList(row)) {
+                        bgc = BGCOLOR_SAME_POSITION_IN_OPPOSITE;
+                    } else if (model.isIncludedInOppositeList(row)) {
+                        bgc = BGCOLOR_IN_OPPOSITE;
+                    } else {
+                        bgc = BGCOLOR_NOT_IN_OPPOSITE;
+                    }
+                }
+            }
+        }
+        setBackground(bgc);
+    }
+
+    protected void renderForeground(ListMergeModel<Node>.EntriesTableModel model, RelationMember member, int row, int col, boolean isSelected) {
+        Color fgc = Color.BLACK;
+        if (col == 0 && model.isParticipatingInCurrentComparePair() && ! model.getListMergeModel().isFrozen()) {
+            fgc = Color.WHITE;
+        }
+        setForeground(fgc);
+    }
+
+    protected void renderRole(RelationMember member) {
+        setText(member.getRole());
+        setToolTipText(member.getRole());
+    }
+
+    protected void renderPrimitive(RelationMember member) {
+        String displayName = member.getMember().getName();
+        setText(displayName);
+        setToolTipText(buildToolTipText(member.getMember()));
+        if (member.isNode()) {
+            setIcon(nodeIcon);
+        } else if (member.isWay()) {
+            setIcon(wayIcon);
+        } else if (member.isRelation()) {
+            setIcon(relationIcon);
+        } else {
+            // should not happen
+            setIcon(null);
+        }
+    }
+
+    /**
+     * render the row id
+     * @param row the row index
+     * @param isSelected
+     */
+    protected  void renderRowId(int row) {
+        setBorder(rowNumberBorder);
+        setText(Integer.toString(row+1));
+    }
+
+    protected void renderEmptyRow() {
+        setIcon(null);
+        setBackground(BGCOLOR_EMPTY_ROW);
+        setText("");
+    }
+
+
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
+            int row, int column) {
+
+        RelationMember member = (RelationMember)value;
+        reset();
+        renderBackground(getModel(table), member, row, column, isSelected);
+        renderForeground(getModel(table), member, row, column, isSelected);
+        switch(column) {
+            case 0:
+                renderRowId(row);
+                break;
+            case 1:
+                if (member == null) {
+                    renderEmptyRow();
+                } else {
+                    renderRole(member);
+                }
+                break;
+            case 2:
+                if (member == null) {
+                    renderEmptyRow();
+                } else {
+                    renderPrimitive(member);
+                }
+                break;
+            default:
+                // should not happen
+        }
+        return this;
+    }
+
+    /**
+     * replies the model
+     * @param table  the table
+     * @return the table model
+     */
+    @SuppressWarnings("unchecked")
+    protected ListMergeModel<Node>.EntriesTableModel getModel(JTable table) {
+        return (ListMergeModel.EntriesTableModel)table.getModel();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MergedTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MergedTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MergedTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,74 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+
+public class MergedTableCellRenderer extends TagMergeTableCellRenderer {
+
+    public final static Color BGCOLOR_UNDECIDED = new Color(255,197,197);
+    public final static Color BGCOLOR_MINE = new Color(217,255,217);
+    public final static Color BGCOLOR_THEIR = new Color(217,255,217);
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+
+    
+    protected void setBackgroundColor(TagMergeItem item, boolean isSelected) {
+        if (isSelected) {
+            setBackground(BGCOLOR_SELECTED);
+            return;
+        }
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_MINE);
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_THEIR); 
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_UNDECIDED);
+        }        
+    }
+    
+    
+    @Override
+    protected void renderKey(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item,isSelected);
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision()) && item.getMyTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("The merged dataset will not include a tag with key {0}", item.getKey()));
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision()) && item.getTheirTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("The merged dataset will not include a tag with key {0}", item.getKey()));
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setText("");
+        } else {
+            setText(item.getKey());
+            setToolTipText(item.getKey());
+        }
+    }
+
+    @Override
+    protected void renderValue(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item,isSelected);
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision()) && item.getMyTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("The merged dataset will not include a tag with key {0}", item.getKey()));
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision()) && item.getTheirTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("The merged dataset will not include a tag with key {0}", item.getKey()));
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setText("");
+        } else {
+            if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+                setText(item.getMyTagValue());
+                setToolTipText(item.getMyTagValue());
+            } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+                setText(item.getTheirTagValue());
+                setToolTipText(item.getTheirTagValue());
+            } else {
+                // should not happen 
+            }
+        }
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MineTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MineTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/MineTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,68 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import java.awt.Color;
+
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+public class MineTableCellRenderer extends TagMergeTableCellRenderer {
+
+    public final static Color BGCOLOR_UNDECIDED = new Color(255,197,197);
+    public final static Color BGCOLOR_MINE = new Color(217,255,217);
+    public final static Color BGCOLOR_THEIR = Color.white;
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+    
+    protected void setBackgroundColor(TagMergeItem item, boolean isSelected) {
+        if (isSelected)  {
+            setBackground(BGCOLOR_SELECTED);
+            return;
+        }
+        
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_MINE);
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_THEIR); 
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_UNDECIDED);
+        }        
+    }
+    
+    protected void setTextColor(TagMergeItem item) {
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+            setForeground(Color.black);
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+            setForeground(Color.LIGHT_GRAY); 
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setForeground(Color.black);
+        }                
+    }
+    
+    @Override
+    protected void renderKey(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item,isSelected);
+        setTextColor(item);
+        if (item.getMyTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("My dataset does not include a tag with key {0}", item.getKey()));
+        } else {
+            setText(item.getKey());    
+            setToolTipText(item.getKey());
+        }
+    }
+
+    @Override
+    protected void renderValue(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item,isSelected);
+        setTextColor(item);
+        if (item.getMyTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("My dataset does not include a tag with key {0}", item.getKey()));
+        } else {
+            setText(item.getMyTagValue());    
+            setToolTipText(item.getMyTagValue());
+        }        
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeColumnModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeColumnModel.java	(revision 1954)
@@ -0,0 +1,34 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+
+public class TagMergeColumnModel extends DefaultTableColumnModel {
+
+    protected void createColumns(TableCellRenderer renderer) {
+        
+        TableColumn col = null;
+        
+        // column 0 - Key   
+        col = new TableColumn(0);
+        col.setHeaderValue(tr("Key"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+        
+        // column 1 - Value   
+        col = new TableColumn(1);
+        col.setHeaderValue(tr("Value"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+    }
+
+    public TagMergeColumnModel(TableCellRenderer renderer) {
+        createColumns(renderer);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeItem.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeItem.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeItem.java	(revision 1954)
@@ -0,0 +1,117 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+
+/**
+ * TagMergeItem represents an individual merge action for a specific pair of key/value.
+ *
+ * A TagMergeItem manages the values of the two key/value-pairs and keeps track of the applied
+ * merge decision.
+ *
+ */
+public class TagMergeItem {
+
+    private String key = null;
+    private String myTagValue = null;
+    private String theirTagValue = null;
+    private MergeDecisionType mergeDecision = MergeDecisionType.UNDECIDED;
+
+    /**
+     * constructor
+     *
+     * @param key  the common tag key. Must not be null.
+     * @param myTagValue  the value for this key known in the local dataset
+     * @param theirTagValue  the value for this key known in the dataset on the server
+     * @throws IllegalArgumentException if key is null
+     */
+    public TagMergeItem(String key, String myTagValue, String theirTagValue) {
+        if (key == null) {
+            throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "key"));
+        }
+        this.key  = key;
+        this.myTagValue = myTagValue;
+        this.theirTagValue = theirTagValue;
+        this.mergeDecision = MergeDecisionType.UNDECIDED;
+    }
+
+    /**
+     * constructor
+     *
+     * @param key  the tag key common to the merged OSM primitives. Must not be null.
+     * @param my  my version of the OSM primitive (i.e. the version known in the local dataset). Must not be null.
+     * @param their their version of the OSM primitive (i.e. the version known on the server). Must not be null.
+     * @throws IllegalArgumentException thrown if key is null
+     * @throws IllegalArgumentException thrown if my is null
+     * @throws IllegalArgumentException thrown if their is null
+     */
+    public TagMergeItem(String key, OsmPrimitive my, OsmPrimitive their) {
+        if (key == null) throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "key"));
+        if (my == null) throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "my"));
+        if (their == null) throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "their"));
+        this.key = key;
+        myTagValue = my.get(key);
+        theirTagValue = their.get(key);
+    }
+
+    /**
+     * applies a merge decision to this merge item
+     *
+     * @param decision the merge decision. Must not be null.
+     * @exception IllegalArgumentException thrown if decision is null
+     *
+     */
+    public void decide(MergeDecisionType decision) throws IllegalArgumentException {
+        if (decision == null) throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "decision"));
+        this.mergeDecision = decision;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getMyTagValue() {
+        return myTagValue;
+    }
+
+    public String getTheirTagValue() {
+        return theirTagValue;
+    }
+
+    public MergeDecisionType getMergeDecision() {
+        return mergeDecision;
+    }
+
+    /**
+     * applies the current merge decisions to the tag set of an OSM primitive. The
+     * OSM primitive has the role of primitive in the local dataset ('my' primitive,
+     * not 'their' primitive)
+     *
+     * @param primitive the OSM primitive. Must not be null.
+     * @exception IllegalArgumentException thrown, if primitive is null
+     * @exception IllegalStateException  thrown, if this merge item is undecided
+     */
+    public void applyToMyPrimitive(OsmPrimitive primitive) throws IllegalArgumentException, IllegalStateException {
+        if (primitive == null) throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "primitive"));
+        if (mergeDecision == MergeDecisionType.UNDECIDED) {
+            throw new IllegalStateException(tr("cannot apply undecided tag merge item"));
+        } else if (mergeDecision == MergeDecisionType.KEEP_THEIR) {
+            if (theirTagValue == null) {
+                primitive.remove(key);
+            } else if (theirTagValue != null) {
+                primitive.put(key, theirTagValue);
+            }
+        } else if (mergeDecision == MergeDecisionType.KEEP_MINE) {
+            if (myTagValue == null) {
+                primitive.remove(key);
+            } else if (myTagValue != null) {
+                primitive.put(key, myTagValue);
+            }
+        } else {
+           // should not happen
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeModel.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeModel.java	(revision 1954)
@@ -0,0 +1,215 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import javax.swing.table.DefaultTableModel;
+
+import org.openstreetmap.josm.command.TagConflictResolveCommand;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+
+/**
+ * This is the {@see TableModel} used in the tables of the {@see TagMerger}.
+ * 
+ * The model can {@see #populate(OsmPrimitive, OsmPrimitive)} itself from the conflicts
+ * in the tag sets of two {@see OsmPrimitive}s. Internally, it keeps a list of {@see TagMergeItem}s.
+ * 
+ *  {@see #decide(int, MergeDecisionType)} and {@see #decide(int[], MergeDecisionType)} can be used
+ *  to remember a merge decision for a specific row in the model.
+ * 
+ *  The model notifies {@see PropertyChangeListener}s about updates of the number of
+ *  undecided tags (see {@see #PROP_NUM_UNDECIDED_TAGS}).
+ * 
+ */
+public class TagMergeModel extends DefaultTableModel {
+    private static final Logger logger = Logger.getLogger(TagMergeModel.class.getName());
+
+    static public final String PROP_NUM_UNDECIDED_TAGS = TagMergeModel.class.getName() + ".numUndecidedTags";
+
+    /** the list of tag merge items */
+    private final ArrayList<TagMergeItem> tagMergeItems;
+
+    /** the property change listeners */
+    private final ArrayList<PropertyChangeListener> listeners;
+
+    private int numUndecidedTags = 0;
+
+
+    public TagMergeModel() {
+        tagMergeItems = new ArrayList<TagMergeItem>();
+        listeners = new ArrayList<PropertyChangeListener>();
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        synchronized(listeners) {
+            if (listener == null) return;
+            if (listeners.contains(listener)) return;
+            listeners.add(listener);
+        }
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        synchronized(listeners) {
+            if (listener == null) return;
+            if (!listeners.contains(listener)) return;
+            listeners.remove(listener);
+        }
+    }
+
+    /**
+     * notifies {@see PropertyChangeListener}s about an update of {@see TagMergeModel#PROP_NUM_UNDECIDED_TAGS}
+
+     * @param oldValue the old value
+     * @param newValue the new value
+     */
+    protected void fireNumUndecidedTagsChanged(int oldValue, int newValue) {
+        PropertyChangeEvent evt = new PropertyChangeEvent(this,PROP_NUM_UNDECIDED_TAGS,oldValue, newValue);
+        synchronized(listeners) {
+            for(PropertyChangeListener l : listeners) {
+                l.propertyChange(evt);
+            }
+        }
+    }
+
+    /**
+     * refreshes the number of undecided tag conflicts after an update in the list of
+     * {@see TagMergeItem}s. Notifies {@see PropertyChangeListener} if necessary.
+     * 
+     */
+    protected void refreshNumUndecidedTags() {
+        int newValue=0;
+        for(TagMergeItem item: tagMergeItems) {
+            if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+                newValue++;
+            }
+        }
+        int oldValue = numUndecidedTags;
+        numUndecidedTags = newValue;
+        fireNumUndecidedTagsChanged(oldValue, numUndecidedTags);
+
+    }
+
+    /**
+     * Populate the model with conflicts between the tag sets of the two
+     * {@see OsmPrimitive} <code>my</code> and <code>their</code>.
+     * 
+     * @param my  my primitive (i.e. the primitive from the local dataset)
+     * @param their their primitive (i.e. the primitive from the server dataset)
+     * 
+     */
+    public void populate(OsmPrimitive my, OsmPrimitive their) {
+        tagMergeItems.clear();
+        Set<String> keys = new HashSet<String>();
+        keys.addAll(my.keySet());
+        keys.addAll(their.keySet());
+        for(String key : keys) {
+            String myValue = my.get(key);
+            String theirValue = their.get(key);
+            if (myValue == null || theirValue == null || ! myValue.equals(theirValue)) {
+                tagMergeItems.add(
+                        new TagMergeItem(key, my, their)
+                );
+            }
+        }
+        fireTableDataChanged();
+        refreshNumUndecidedTags();
+    }
+
+    /**
+     * add a {@see TagMergeItem} to the model
+     * 
+     * @param item the item
+     */
+    public void addItem(TagMergeItem item) {
+        if (item != null) {
+            tagMergeItems.add(item);
+            fireTableDataChanged();
+            refreshNumUndecidedTags();
+        }
+    }
+
+    protected void rememberDecision(int row, MergeDecisionType decision) {
+        TagMergeItem item = tagMergeItems.get(row);
+        item.decide(decision);
+    }
+
+    /**
+     * set the merge decision of the {@see TagMergeItem} in row <code>row</code>
+     * to <code>decision</code>.
+     * 
+     * @param row  the row
+     * @param decision the decision
+     */
+    public void decide(int row, MergeDecisionType decision) {
+        rememberDecision(row, decision);
+        fireTableRowsUpdated(row, row);
+        refreshNumUndecidedTags();
+    }
+
+    /**
+     * set the merge decision of all {@see TagMergeItem} given by indices in <code>rows</code>
+     * to <code>decision</code>.
+     * 
+     * @param row  the array of row indices
+     * @param decision the decision
+     */
+
+    public void decide(int [] rows, MergeDecisionType decision) {
+        if (rows == null || rows.length == 0)
+            return;
+        for (int row : rows) {
+            rememberDecision(row, decision);
+        }
+        fireTableDataChanged();
+        refreshNumUndecidedTags();
+    }
+
+    @Override
+    public int getRowCount() {
+        return tagMergeItems == null ? 0 : tagMergeItems.size();
+    }
+
+
+    @Override
+    public Object getValueAt(int row, int column) {
+        // return the tagMergeItem for both columns. The cell
+        // renderer will dispatch on the column index and get
+        // the key or the value from the TagMergeItem
+        //
+        return tagMergeItems.get(row);
+    }
+
+    @Override
+    public boolean isCellEditable(int row, int column) {
+        return false;
+    }
+
+    public TagConflictResolveCommand buildResolveCommand(OsmPrimitive my, OsmPrimitive their) {
+        return new TagConflictResolveCommand(my,  their, tagMergeItems);
+    }
+
+    public boolean isResolvedCompletely() {
+        for (TagMergeItem item: tagMergeItems) {
+            if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED))
+                return false;
+        }
+        return true;
+    }
+
+    public int getNumResolvedConflicts() {
+        int n = 0;
+        for (TagMergeItem item: tagMergeItems) {
+            if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) {
+                n++;
+            }
+        }
+        return n;
+
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMergeTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,43 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.table.TableCellRenderer;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+public abstract class TagMergeTableCellRenderer extends JLabel implements TableCellRenderer {
+
+    protected  abstract void renderKey(TagMergeItem item, boolean isSelected );
+    
+    protected abstract void renderValue(TagMergeItem item, boolean isSelected);
+    
+    protected void reset() {
+        setOpaque(true);
+        setBackground(Color.white);
+        setForeground(Color.black);
+    }
+    
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row,
+            int col) {
+        
+        reset();        
+        TagMergeItem item = (TagMergeItem)value;
+        switch(col) {
+        case 0: 
+            renderKey(item, isSelected);
+            break;
+        case 1:
+            renderValue(item, isSelected);
+            break;
+        default:
+            // should not happen, but just in case 
+            throw new IllegalArgumentException(tr("Parameter 'col' must be 0 or 1. Got {0}", col));
+        }
+        return this;
+    }
+    
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMerger.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TagMerger.java	(revision 1954)
@@ -0,0 +1,397 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Adjustable;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+import org.openstreetmap.josm.tools.ImageProvider;
+/**
+ * UI component for resolving conflicts in the tag sets of two {@see OsmPrimitive}s.
+ *
+ */
+public class TagMerger extends JPanel {
+
+    private JTable mineTable;
+    private JTable mergedTable;
+    private JTable theirTable;
+    private final TagMergeModel model;
+    private JButton btnKeepMine;
+    private JButton btnKeepTheir;
+    AdjustmentSynchronizer adjustmentSynchronizer;
+
+    /**
+     * embeds table in a new {@see JScrollPane} and returns th scroll pane
+     * 
+     * @param table the table
+     * @return the scroll pane embedding the table
+     */
+    protected JScrollPane embeddInScrollPane(JTable table) {
+        JScrollPane pane = new JScrollPane(table);
+        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+
+        adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
+        return pane;
+    }
+
+    /**
+     * builds the table for my tag set (table already embedded in a scroll pane)
+     * 
+     * @return the table (embedded in a scroll pane)
+     */
+    protected JScrollPane buildMineTagTable() {
+        mineTable  = new JTable(
+                model,
+                new TagMergeColumnModel(
+                        new MineTableCellRenderer()
+                )
+        );
+        mineTable.setName("table.my");
+        return embeddInScrollPane(mineTable);
+    }
+
+    /**
+     * builds the table for their tag set (table already embedded in a scroll pane)
+     * 
+     * @return the table (embedded in a scroll pane)
+     */
+    protected JScrollPane buildTheirTable() {
+        theirTable  = new JTable(
+                model,
+                new TagMergeColumnModel(
+                        new TheirTableCellRenderer()
+                )
+        );
+        theirTable.setName("table.their");
+        return embeddInScrollPane(theirTable);
+    }
+
+    /**
+     * builds the table for the merged tag set (table already embedded in a scroll pane)
+     * 
+     * @return the table (embedded in a scroll pane)
+     */
+
+    protected JScrollPane buildMergedTable() {
+        mergedTable  = new JTable(
+                model,
+                new TagMergeColumnModel(
+                        new MergedTableCellRenderer()
+                )
+        );
+        mergedTable.setName("table.merged");
+        return embeddInScrollPane(mergedTable);
+    }
+
+    /**
+     * build the user interface
+     */
+    protected void build() {
+        GridBagConstraints gc = new GridBagConstraints();
+        setLayout(new GridBagLayout());
+
+        adjustmentSynchronizer = new AdjustmentSynchronizer();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(10,0,10,0);
+        JLabel lbl = new JLabel(tr("My version (local dataset)"));
+        add(lbl, gc);
+
+        gc.gridx = 2;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        lbl = new JLabel(tr("Merged version"));
+        add(lbl, gc);
+
+        gc.gridx = 4;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0,0,0,0);
+        lbl = new JLabel(tr("Their version (server dataset)"));
+        add(lbl, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.3;
+        gc.weighty = 1.0;
+        add(buildMineTagTable(), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepMineAction keepMineAction = new KeepMineAction();
+        mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
+        btnKeepMine = new JButton(keepMineAction);
+        btnKeepMine.setName("button.keepmine");
+        add(btnKeepMine, gc);
+
+        gc.gridx = 2;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.3;
+        gc.weighty = 1.0;
+        add(buildMergedTable(), gc);
+
+        gc.gridx = 3;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        KeepTheirAction keepTheirAction = new KeepTheirAction();
+        btnKeepTheir = new JButton(keepTheirAction);
+        btnKeepTheir.setName("button.keeptheir");
+        add(btnKeepTheir, gc);
+
+        gc.gridx = 4;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.3;
+        gc.weighty = 1.0;
+        add(buildTheirTable(), gc);
+        theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
+
+
+        DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
+        mineTable.addMouseListener(dblClickAdapter);
+        theirTable.addMouseListener(dblClickAdapter);
+
+
+        gc.gridx = 2;
+        gc.gridy = 2;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        UndecideAction undecidedAction = new UndecideAction();
+        mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
+        JButton btnUndecide = new JButton(undecidedAction);
+        btnUndecide.setName("button.undecide");
+        add(btnUndecide, gc);
+
+    }
+
+    public TagMerger() {
+        model = new TagMergeModel();
+        build();
+    }
+
+    /**
+     * replies the model used by this tag merger
+     * 
+     * @return the model
+     */
+    public TagMergeModel getModel() {
+        return model;
+    }
+
+    /**
+     * Keeps the currently selected tags in my table in the list of merged tags.
+     *
+     */
+    class KeepMineAction extends AbstractAction implements ListSelectionListener {
+        public KeepMineAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeepmine.png");
+            if (icon != null) {
+                putValue(Action.SMALL_ICON, icon);
+                putValue(Action.NAME, "");
+            } else {
+                putValue(Action.NAME, tr(">"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = mineTable.getSelectedRows();
+            if (rows == null || rows.length == 0)
+                return;
+            model.decide(rows, MergeDecisionType.KEEP_MINE);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(mineTable.getSelectedRowCount() > 0);
+        }
+    }
+
+    /**
+     * Keeps the currently selected tags in their table in the list of merged tags.
+     *
+     */
+    class KeepTheirAction extends AbstractAction implements ListSelectionListener {
+        public KeepTheirAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeeptheir.png");
+            if (icon != null) {
+                putValue(Action.SMALL_ICON, icon);
+                putValue(Action.NAME, "");
+            } else {
+                putValue(Action.NAME, tr(">"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = theirTable.getSelectedRows();
+            if (rows == null || rows.length == 0)
+                return;
+            model.decide(rows, MergeDecisionType.KEEP_THEIR);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(theirTable.getSelectedRowCount() > 0);
+        }
+    }
+
+    /**
+     * Synchronizes scrollbar adjustments between a set of
+     * {@see Adjustable}s. Whenever the adjustment of one of
+     * the registerd Adjustables is updated the adjustment of
+     * the other registered Adjustables is adjusted too.
+     * 
+     */
+    class AdjustmentSynchronizer implements AdjustmentListener {
+        private final ArrayList<Adjustable> synchronizedAdjustables;
+
+        public AdjustmentSynchronizer() {
+            synchronizedAdjustables = new ArrayList<Adjustable>();
+        }
+
+        public void synchronizeAdjustment(Adjustable adjustable) {
+            if (adjustable == null)
+                return;
+            if (synchronizedAdjustables.contains(adjustable))
+                return;
+            synchronizedAdjustables.add(adjustable);
+            adjustable.addAdjustmentListener(this);
+        }
+
+        public void adjustmentValueChanged(AdjustmentEvent e) {
+            for (Adjustable a : synchronizedAdjustables) {
+                if (a != e.getAdjustable()) {
+                    a.setValue(e.getValue());
+                }
+            }
+        }
+    }
+
+    /**
+     * Handler for double clicks on entries in the three tag tables.
+     * 
+     */
+    class DoubleClickAdapter extends MouseAdapter {
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            if (e.getClickCount() != 2)
+                return;
+            JTable table = null;
+            MergeDecisionType mergeDecision;
+
+            if (e.getSource() == mineTable) {
+                table = mineTable;
+                mergeDecision = MergeDecisionType.KEEP_MINE;
+            } else if (e.getSource() == theirTable) {
+                table = theirTable;
+                mergeDecision = MergeDecisionType.KEEP_THEIR;
+            } else if (e.getSource() == mergedTable) {
+                table = mergedTable;
+                mergeDecision = MergeDecisionType.UNDECIDED;
+            } else
+                // double click in another component; shouldn't happen,
+                // but just in case
+                return;
+            int row = table.rowAtPoint(e.getPoint());
+            model.decide(row, mergeDecision);
+        }
+    }
+
+    /**
+     * Sets the currently selected tags in the table of merged tags to state
+     * {@see MergeDecisionType#UNDECIDED}
+     * 
+     */
+    class UndecideAction extends AbstractAction implements ListSelectionListener  {
+
+        public UndecideAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagundecide.png");
+            if (icon != null) {
+                putValue(Action.SMALL_ICON, icon);
+                putValue(Action.NAME, "");
+            } else {
+                putValue(Action.NAME, tr("Undecide"));
+            }
+            putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
+            setEnabled(false);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = mergedTable.getSelectedRows();
+            if (rows == null || rows.length == 0)
+                return;
+            model.decide(rows, MergeDecisionType.UNDECIDED);
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(mergedTable.getSelectedRowCount() > 0);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TheirTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TheirTableCellRenderer.java	(revision 1954)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/pair/tags/TheirTableCellRenderer.java	(revision 1954)
@@ -0,0 +1,67 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+
+import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
+
+public class TheirTableCellRenderer extends TagMergeTableCellRenderer {
+
+    public final static Color BGCOLOR_UNDECIDED = new Color(255,197,197);
+    public final static Color BGCOLOR_MINE = Color.white;
+    public final static Color BGCOLOR_THEIR = new Color(217,255,217);
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+
+    
+    protected void setBackgroundColor(TagMergeItem item, boolean isSelected) {
+        if (isSelected) {
+            setBackground(BGCOLOR_SELECTED);
+            return;
+        }
+        if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_MINE);
+        } else if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_THEIR); 
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setBackground(BGCOLOR_UNDECIDED);
+        }        
+    }
+    
+    protected void setTextColor(TagMergeItem item) {
+        if (MergeDecisionType.KEEP_THEIR.equals(item.getMergeDecision())) {
+            setForeground(Color.black);
+        } else if (MergeDecisionType.KEEP_MINE.equals(item.getMergeDecision())) {
+            setForeground(Color.LIGHT_GRAY); 
+        } else if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
+            setForeground(Color.black);
+        }                
+    }
+    
+    @Override
+    protected void renderKey(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item, isSelected);
+        setTextColor(item);
+        if (item.getTheirTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("Their dataset does not include a tag with key {0}", item.getKey()));
+        } else {
+            setText(item.getKey());    
+            setToolTipText(item.getKey());
+        }
+    }
+
+    @Override
+    protected void renderValue(TagMergeItem item, boolean isSelected) {
+        setBackgroundColor(item,isSelected);
+        setTextColor(item);
+        if (item.getTheirTagValue() == null) {
+            setText(tr("<undefined>"));
+            setToolTipText(tr("Their dataset does not include a tag with key {0}", item.getKey()));
+        } else {
+            setText(item.getTheirTagValue());    
+            setToolTipText(item.getTheirTagValue());
+        }        
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java	(revision 1954)
@@ -27,6 +27,6 @@
 import org.openstreetmap.josm.gui.OptionPaneUtil;
 import org.openstreetmap.josm.gui.PrimitiveNameFormatter;
-import org.openstreetmap.josm.gui.conflict.ConflictResolver;
-import org.openstreetmap.josm.gui.conflict.properties.OperationCancelledException;
+import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver;
+import org.openstreetmap.josm.gui.conflict.pair.properties.OperationCancelledException;
 import org.openstreetmap.josm.tools.ImageProvider;
 
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 1953)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 1954)
@@ -9,6 +9,4 @@
 import java.awt.Point;
 import java.awt.event.ActionEvent;
-import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
@@ -23,5 +21,4 @@
 
 import javax.swing.AbstractAction;
-import javax.swing.Action;
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.DefaultListModel;
@@ -273,7 +270,4 @@
      */
     public final  class DeleteLayerAction extends AbstractAction implements IEnabledStateUpdating {
-
-
-
         private  Layer layer;
 
