Index: trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 1622)
@@ -0,0 +1,126 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.MutableTreeNode;
+
+import org.openstreetmap.josm.Main;
+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.tags.MergeDecisionType;
+import org.openstreetmap.josm.gui.conflict.tags.TagMergeItem;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Represents a the resolution of a tag conflict in an {@see OsmPrimitive}
+ *
+ */
+public class TagConflictResolveCommand extends Command {
+    
+    /** my primitive (in the local dataset). merge decisions are applied to this
+     *  primitive
+     */
+    private OsmPrimitive my;
+    /** their primitive (in the server dataset) */ 
+    private OsmPrimitive their;
+    
+    /** the list of merge decisions, represented as {@see TagMergeItem}s */
+    private List<TagMergeItem> mergeItems; 
+    
+    /**
+     * replies the number of decided conflicts
+     * 
+     * @return the number of decided conflicts 
+     */
+    public int getNumDecidedConflicts() {
+        int n = 0;
+        for (TagMergeItem item: mergeItems) {
+            if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) {
+                n++; 
+            }
+        }
+        return n;
+    }
+    
+    /**
+     * replies a (localized) display name for the type of an OSM primitive
+     * 
+     * @param primitive the primitive
+     * @return a localized display name
+     */
+    protected String getPrimitiveTypeAsString(OsmPrimitive primitive) {
+        if (primitive instanceof Node) return tr("node");
+        if (primitive instanceof Way) return tr("way");
+        if (primitive instanceof Relation) return tr("relation");
+        return "";
+    }
+    
+    /**
+     * constructor 
+     * 
+     * @param my  my primitive
+     * @param their  their primitive 
+     * @param mergeItems the list of merge decisions, represented as {@see TagMergeItem}s
+     */
+    public TagConflictResolveCommand(OsmPrimitive my, OsmPrimitive their, List<TagMergeItem> mergeItems) {
+        this.my = my;
+        this.their = their;
+        this.mergeItems = mergeItems;
+    }
+    
+    
+    @Override
+    public MutableTreeNode description() {                
+        return new DefaultMutableTreeNode(
+          new JLabel(
+             tr("Resolve {0} tag conflicts in {1} {2}",getNumDecidedConflicts(), getPrimitiveTypeAsString(my), my.id), 
+             ImageProvider.get("data", "object"), 
+             JLabel.HORIZONTAL
+          )
+       );
+    }
+
+    @Override
+    public boolean executeCommand() {
+        // remember the current state of modified primitives, i.e. of
+        // OSM primitive 'my'
+        //
+        super.executeCommand();
+        
+        // apply the merge decisions to OSM primitive 'my'
+        //
+        for (TagMergeItem item: mergeItems) {
+            item.applyToMyPrimitive(my);
+        }
+        return true;
+    }
+
+    @Override
+    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
+            Collection<OsmPrimitive> added) {
+        modified.add(my);
+    }
+
+    @Override
+    public void undoCommand() {
+        // restore former state of modified primitives
+        //
+        super.undoCommand();
+        
+        // restore a conflict if necessary
+        //
+        if (!Main.map.conflictDialog.conflicts.containsKey(my)) {
+            Main.map.conflictDialog.conflicts.put(my,their);
+        }
+    }
+
+    
+}
Index: trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java	(revision 1622)
@@ -0,0 +1,90 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+
+import javax.swing.JLabel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.MutableTreeNode;
+
+import org.openstreetmap.josm.Main;
+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.tools.ImageProvider;
+
+/**
+ * Represents a command for resolving a version conflict between two {@see OsmPrimitive}
+ *
+ *
+ */
+public class VersionConflictResolveCommand extends Command {
+
+    private OsmPrimitive my;
+    private OsmPrimitive their;
+    
+    /**
+     * constructor 
+     * @param my  my primitive (i.e. the primitive from the local dataset) 
+     * @param their their primitive (i.e. the primitive from the server) 
+     */
+    public VersionConflictResolveCommand(OsmPrimitive my, OsmPrimitive their) {
+      this.my = my;
+      this.their = their; 
+    }
+    
+    //FIXME copied from TagConflictResolveCommand -> refactor
+    /**
+     * replies a (localized) display name for the type of an OSM primitive
+     * 
+     * @param primitive the primitive
+     * @return a localized display name
+     */
+    protected String getPrimitiveTypeAsString(OsmPrimitive primitive) {
+        if (primitive instanceof Node) return tr("node");
+        if (primitive instanceof Way) return tr("way");
+        if (primitive instanceof Relation) return tr("relation");
+        return "";
+    }
+    
+    @Override
+    public MutableTreeNode description() {
+        return new DefaultMutableTreeNode(
+            new JLabel(
+               tr("Resolve version conflicts for {0} {1}",getPrimitiveTypeAsString(my), my.id), 
+               ImageProvider.get("data", "object"), 
+               JLabel.HORIZONTAL
+            )
+         );
+    }
+
+    @Override
+    public boolean executeCommand() {
+        super.executeCommand();
+        my.version = Math.max(my.version, their.version);
+        Main.map.conflictDialog.conflicts.remove(my);
+        return true;
+    }
+
+    @Override
+    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
+            Collection<OsmPrimitive> added) {
+        modified.add(my);
+    }
+
+    @Override
+    public void undoCommand() {
+        super.undoCommand();
+        
+        // restore a conflict if necessary
+        //
+        if (!Main.map.conflictDialog.conflicts.containsKey(my)) {
+            Main.map.conflictDialog.conflicts.put(my,their);
+        }
+    }
+
+    
+}
Index: trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java	(revision 1622)
@@ -0,0 +1,97 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.MutableTreeNode;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Represent a command for resolving conflicts in the node list of two
+ * {@see Way}s.
+ *
+ */
+public class WayNodesConflictResolverCommand extends Command {
+
+    /** my way */
+    private Way my;
+    /** their way */ 
+    private Way their;
+    /** the list of merged nodes. This becomes the list of news of my way after the
+     *  command is executed
+     */
+    private List<Node> mergedNodeList; 
+    
+    /**
+     * 
+     * @param my my may
+     * @param their their way
+     * @param mergedNodeList  the list of merged nodes 
+     */
+    public WayNodesConflictResolverCommand(Way my, Way their, List<Node> mergedNodeList) {
+        this.my = my;
+        this.their = their;
+        this.mergedNodeList = mergedNodeList;
+    }
+    
+    
+    @Override
+    public MutableTreeNode description() {
+        return new DefaultMutableTreeNode(
+                new JLabel(
+                   tr("Resolve conflicts in node list of of way {0}", my.id), 
+                   ImageProvider.get("data", "object"), 
+                   JLabel.HORIZONTAL
+                )
+         );
+    }
+
+    @Override
+    public boolean executeCommand() {
+        // remember the current state of 'my' way
+        //
+        super.executeCommand();
+        
+        // replace the list of nodes of 'my' way by the list of merged
+        // nodes 
+        //
+        my.nodes.clear();
+        for (int i=0; i<mergedNodeList.size();i++) {
+            Node n = mergedNodeList.get(i);
+            my.nodes.add(n);
+            if (! Main.ds.nodes.contains(n)) {
+                System.out.println("Main.ds doesn't include node " + n.toString());
+            }
+        }
+        return true;        
+    }
+
+    @Override
+    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
+            Collection<OsmPrimitive> added) {
+        modified.add(my);        
+    }
+
+    @Override
+    public void undoCommand() {
+        // restore the former state
+        //
+        super.undoCommand();
+        
+        // restore a conflict if necessary
+        //
+        if (!Main.map.conflictDialog.conflicts.containsKey(my)) {
+            Main.map.conflictDialog.conflicts.put(my,their);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/ConflictResolver.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/ConflictResolver.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/ConflictResolver.java	(revision 1622)
@@ -0,0 +1,167 @@
+// 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.net.URL;
+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.TagConflictResolveCommand;
+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.tags.TagMergeModel;
+import org.openstreetmap.josm.gui.conflict.tags.TagMerger;
+
+/**
+ * An UI component for resolving conflicts between two {@see OsmPrimitive}s.
+ *   
+ *
+ */
+public class ConflictResolver extends JPanel implements PropertyChangeListener  {
+    
+   private static final Logger logger = Logger.getLogger(ConflictResolver.class.getName());
+
+    private JTabbedPane tabbedPane = null;
+    private TagMerger tagMerger;
+    private NodeListMerger nodeListMerger;
+    private OsmPrimitive my;
+    private OsmPrimitive their;
+    
+    private ImageIcon mergeComplete;
+    private ImageIcon mergeIncomplete;
+    
+    // FIXME copied code -> refactor
+    /**
+     * load an icon given by iconName 
+     * 
+     * @param iconName  the name of the icon (without path, i.e. <tt>copystartleft.png</tt>
+     * @return the icon; null, if the icon was not found 
+     */
+    protected ImageIcon getIcon(String iconName) {
+        String fullIconName  = "/images/dialogs/conflict/" + iconName;
+        URL imageURL   = this.getClass().getResource(fullIconName);            
+        if (imageURL == null) {
+            System.out.println(tr("WARNING: failed to load resource {0}", fullIconName));
+            return null;
+        }
+        return new ImageIcon(imageURL);
+    }
+    
+    protected void loadIcons() {
+        mergeComplete = getIcon("mergecomplete.png");
+        mergeIncomplete = getIcon("mergeincomplete.png");
+    }
+    
+    protected void build() {
+        tabbedPane = new JTabbedPane();
+        
+        tagMerger = new TagMerger();
+        tagMerger.getModel().addPropertyChangeListener(this);
+        tabbedPane.add("Tags", tagMerger);
+        
+        nodeListMerger = new NodeListMerger();
+        nodeListMerger.getModel().addPropertyChangeListener(this);
+        tabbedPane.add("Nodes", nodeListMerger);
+        
+        tabbedPane.add("Members", new JPanel());
+        
+        setLayout(new BorderLayout());
+        add(tabbedPane, BorderLayout.CENTER);
+    }
+    
+    
+    public ConflictResolver() {
+        build();
+        loadIcons();
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        
+        if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
+            int newValue = (Integer)evt.getNewValue();
+            if (newValue == 0) {
+                tabbedPane.setTitleAt(0, tr("Tags"));
+                tabbedPane.setToolTipTextAt(0, tr("No pending tag conflicts to be resolved"));
+                tabbedPane.setIconAt(0, mergeComplete);
+            } else {
+                tabbedPane.setTitleAt(0, tr("Tags({0} conflicts)", newValue));
+                tabbedPane.setToolTipTextAt(0, tr("{0} pending tag conflicts to be resolved"));
+                tabbedPane.setIconAt(0, mergeIncomplete);
+            }
+        } else if (evt.getPropertyName().equals(NodeListMergeModel.PROP_FROZEN)) {
+            boolean frozen = (Boolean)evt.getNewValue();
+            if (frozen) {
+                tabbedPane.setTitleAt(1, tr("Nodes(resolved)"));
+                tabbedPane.setToolTipTextAt(1, tr("Pending conflicts in the node list of this way"));
+                tabbedPane.setIconAt(1, mergeComplete);
+            } else {
+                tabbedPane.setTitleAt(1, tr("Nodes(with conflicts)"));
+                tabbedPane.setToolTipTextAt(1, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
+                tabbedPane.setIconAt(1, mergeIncomplete);
+            }
+        }
+    }
+    
+
+    public void populate(OsmPrimitive my, OsmPrimitive their) { 
+        this.my = my;
+        this.their =  their; 
+        tagMerger.getModel().populate(my, their);
+        if (my instanceof Way) {
+           nodeListMerger.populate((Way)my, (Way)their);
+           tabbedPane.setEnabledAt(1, true);
+           tabbedPane.setEnabledAt(2, false);
+        } else if (my instanceof Relation) {
+            tabbedPane.setEnabledAt(1, false);
+            tabbedPane.setEnabledAt(2, true);        
+         }
+    }
+    
+    
+    
+    public Command buildResolveCommand() {
+        ArrayList<Command> commands = new ArrayList<Command>();
+        TagConflictResolveCommand cmd = tagMerger.getModel().buildResolveCommand(my, their);
+        commands.add(cmd);
+        if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
+            commands.add(nodeListMerger.getModel().buildResolveCommand((Way)my, (Way)their));            
+        }
+        if (my instanceof Node) {
+            // resolve the version conflict if this is a node and all tag 
+            // conflicts have been resolved 
+            // 
+            if (tagMerger.getModel().isResolvedCompletely()) {
+                commands.add(
+                   new VersionConflictResolveCommand(my, their)
+                );
+            }
+        } 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 
+            // 
+            if (tagMerger.getModel().isResolvedCompletely() && nodeListMerger.getModel().isFrozen()) {
+                commands.add(
+                   new VersionConflictResolveCommand(my, their)
+                );
+            }            
+        }
+        return new SequenceCommand("Conflict Resolution", commands);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListColumnModel.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListColumnModel.java	(revision 1622)
@@ -0,0 +1,27 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.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 - Node  
+        col = new TableColumn(0);
+        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/nodes/NodeListMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListMergeModel.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListMergeModel.java	(revision 1622)
@@ -0,0 +1,487 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.nodes;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableModel;
+
+import org.openstreetmap.josm.command.WayNodesConflictResolverCommand;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class NodeListMergeModel {
+    private static final Logger logger = Logger.getLogger(NodeListMergeModel.class.getName());
+    
+    public static final String PROP_FROZEN = NodeListMergeModel.class.getName() + ".frozen";
+    
+
+    private ArrayList<Node> myNodes;
+    private ArrayList<Node> theirNodes;
+    private ArrayList<Node> mergedNodes;
+    
+    
+    private DefaultTableModel myNodesTableModel;
+    private DefaultTableModel theirNodesTableModel;
+    private DefaultTableModel mergedNodesTableModel;
+    
+    private DefaultListSelectionModel myNodesSelectionModel;
+    private DefaultListSelectionModel theirNodesSelectionModel;
+    private DefaultListSelectionModel mergedNodesSelectionModel;
+    
+    private ArrayList<PropertyChangeListener> listeners;
+    private boolean isFrozen = false; 
+    
+    
+    public NodeListMergeModel() {
+        myNodes = new ArrayList<Node>();
+        theirNodes = new ArrayList<Node>();
+        mergedNodes = new ArrayList<Node>();
+        
+        myNodesTableModel = new NodeListTableModel(myNodes);
+        theirNodesTableModel = new NodeListTableModel(theirNodes);
+        mergedNodesTableModel = new NodeListTableModel(mergedNodes);
+        
+        myNodesSelectionModel = new DefaultListSelectionModel();
+        theirNodesSelectionModel = new DefaultListSelectionModel();
+        mergedNodesSelectionModel = new DefaultListSelectionModel();
+        
+        listeners = new ArrayList<PropertyChangeListener>();
+        
+        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, PROP_FROZEN, 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 getMyNodesTableModel() {
+        return myNodesTableModel;
+    }
+    
+    public TableModel getTheirNodesTableModel() {
+        return theirNodesTableModel;
+    }
+    
+    public TableModel getMergedNodesTableModel() {
+        return mergedNodesTableModel;
+    }
+    
+    public ListSelectionModel getMyNodesSelectionModel() {
+        return myNodesSelectionModel;
+    }
+
+    public ListSelectionModel getTheirNodesSelectionModel() {
+        return theirNodesSelectionModel;
+    }
+    
+    public ListSelectionModel getMergedNodesSelectionModel() {
+        return mergedNodesSelectionModel;
+    }
+    
+    
+    protected void fireModelDataChanged() {
+        myNodesTableModel.fireTableDataChanged();
+        theirNodesTableModel.fireTableDataChanged();
+        mergedNodesTableModel.fireTableDataChanged();
+    }
+    
+    protected void copyNodesToTop(List<Node> source, int []rows) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        for (int i = rows.length - 1; i >= 0; i--) {
+            int row = rows[i];
+            Node n = source.get(row);
+            mergedNodes.add(0, n);
+        }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.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 copyMyNodesToTop(int [] rows) {
+        copyNodesToTop(myNodes, 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 copyTheirNodesToTop(int [] rows) {
+        copyNodesToTop(theirNodes, 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 copyNodesToEnd(List<Node> source, int [] rows) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        for (int row : rows) {
+            Node n = source.get(row);
+            mergedNodes.add(n);
+        }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.setSelectionInterval(mergedNodes.size()-rows.length, mergedNodes.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 copyMyNodesToEnd(int [] rows) {
+        copyNodesToEnd(myNodes, 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 copyTheirNodesToEnd(int [] rows) {
+        copyNodesToEnd(theirNodes, 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 copyNodesBeforeCurrent(List<Node> source, int [] rows, int current) {
+        if (rows == null || rows.length == 0) {
+            return; 
+        }
+        if (current < 0 || current >= mergedNodes.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];
+            Node n = source.get(row);
+            mergedNodes.add(current, n);
+        }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.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 copyMyNodesBeforeCurrent(int [] rows, int current) {
+        copyNodesBeforeCurrent(myNodes,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 copyTheirNodesBeforeCurrent(int [] rows, int current) {
+        copyNodesBeforeCurrent(theirNodes,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 copyNodesAfterCurrent(List<Node> source, int [] rows, int current) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        if (current < 0 || current >= mergedNodes.size()) {
+            throw new IllegalArgumentException(tr("parameter current out of range: got {0}", current));
+        }
+        if (current == mergedNodes.size() -1) {
+            copyMyNodesToEnd(rows);
+        } else {
+            for (int i=rows.length -1; i>=0; i--) {
+                int row = rows[i];
+                Node n = source.get(row); 
+                mergedNodes.add(current+1, n);
+            }
+        }
+        fireModelDataChanged();   
+        mergedNodesSelectionModel.setSelectionInterval(current+1, 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 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 copyMyNodesAfterCurrent(int [] rows, int current) {
+        copyNodesAfterCurrent(myNodes, 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 copyTheirNodesAfterCurrent(int [] rows, int current) {
+        copyNodesAfterCurrent(theirNodes, rows, current);
+    }
+
+    /**
+     * Moves the nodes given by indices in rows  up by one position in the list
+     * of merged nodes.
+     * 
+     * @param rows the indices 
+     * 
+     */
+    protected void moveUpMergedNodes(int [] rows) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        if (rows[0] == 0) {
+            // can't move up
+            return;
+        }
+        for (int row: rows) {
+           Node n = mergedNodes.get(row);
+           mergedNodes.remove(row);
+           mergedNodes.add(row -1, n);
+        }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedNodesSelectionModel.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 
+     */
+    protected void moveDownMergedNodes(int [] rows) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        if (rows[rows.length -1] == mergedNodes.size() -1) {
+            // can't move down
+            return;
+        }
+        for (int i = rows.length-1; i>=0;i--) {
+            int row = rows[i];
+            Node n = mergedNodes.get(row);
+            mergedNodes.remove(row);
+            mergedNodes.add(row +1, n);
+         }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedNodesSelectionModel.addSelectionInterval(row+1, row+1);
+        }        
+    }
+    
+    /**
+     * Removes the nodes given by indices in rows from the list
+     * of merged nodes.
+     * 
+     * @param rows the indices 
+     */    
+    protected void removeMergedNodes(int [] rows) {
+        if (rows == null || rows.length == 0) {
+            return;
+        }
+        for (int i = rows.length-1; i>=0;i--) {
+            mergedNodes.remove(rows[i]);
+         }
+        fireModelDataChanged();
+        mergedNodesSelectionModel.clearSelection();
+    }
+    
+
+    /**
+     * Replies true if the list of my nodes and the list of their
+     * nodes are equal, i.e. if they consists of a list of nodes with
+     * identical ids in the same order.
+     * 
+     * @return true, if the lists are equal; false otherwise 
+     */
+    protected boolean myAndTheirNodesEqual() {
+        if (myNodes.size() != theirNodes.size()) {
+            return false;
+        }
+        for (int i=0; i < myNodes.size(); i++) {
+            if (myNodes.get(i).id != theirNodes.get(i).id) {
+                return false; 
+            }
+        }
+        return true; 
+    }
+    
+    /**
+     * 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("parameter 'way' must not be null");
+        if (their == null) 
+            throw new IllegalArgumentException("parameter 'their' must not be null");
+        mergedNodes.clear();
+        myNodes.clear();
+        theirNodes.clear();
+        for (Node n : my.nodes) {
+            myNodes.add(n);
+        }
+        for (Node n : their.nodes) {
+            theirNodes.add(n);
+        }
+        if (myAndTheirNodesEqual()) {
+            mergedNodes = new ArrayList<Node>(myNodes);
+            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("parameter my most not be null");            
+        }
+        if (their == null) {
+            throw new IllegalArgumentException("parameter my most not be null");            
+        }
+        if (! isFrozen()) {
+            throw new IllegalArgumentException("merged nodes not frozen yet. Can't build resolution command");
+        }
+        return new WayNodesConflictResolverCommand(my, their, mergedNodes);
+    }
+    
+    class NodeListTableModel extends DefaultTableModel {
+        private ArrayList<Node> nodes;
+        
+        public NodeListTableModel(ArrayList<Node> nodes) {
+            this.nodes = nodes; 
+        }
+        
+        @Override
+        public int getRowCount() {
+            return nodes == null ? 0 : nodes.size();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            return nodes.get(row);           
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }  
+    }    
+
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListMerger.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListMerger.java	(revision 1622)
@@ -0,0 +1,756 @@
+package org.openstreetmap.josm.gui.conflict.nodes;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.net.URL;
+import java.util.logging.Logger;
+
+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.JToggleButton;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import org.openstreetmap.josm.data.osm.Way;
+
+/**
+ * A UI component for resolving conflicts in the node lists of two {@see Way}s.
+ * 
+ */
+public class NodeListMerger extends JPanel implements PropertyChangeListener {
+    private static final Logger logger = Logger.getLogger(NodeListMerger.class.getName());
+    
+    private JTable myNodes;
+    private JTable mergedNodes;
+    private JTable theirNodes;
+    
+    private NodeListMergeModel 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;
+    
+
+    
+    protected JScrollPane embeddInScrollPane(JTable table) {
+        JScrollPane pane = new JScrollPane(table);
+        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+       return pane;
+    }
+    
+    protected JScrollPane buildMyNodesTable() {
+        myNodes  = new JTable(
+            model.getMyNodesTableModel(),
+            new NodeListColumnModel(
+               new NodeListTableCellRenderer()
+            ),
+            model.getMyNodesSelectionModel()
+         );
+         myNodes.setName("table.mynodes");
+         return embeddInScrollPane(myNodes);
+    }
+
+    protected JScrollPane buildMergedNodesTable() {
+        mergedNodes  = new JTable(
+            model.getMergedNodesTableModel(),
+            new NodeListColumnModel(
+                new NodeListTableCellRenderer()
+            ),
+            model.getMergedNodesSelectionModel()
+         );
+         mergedNodes.setName("table.mergednodes");
+         return embeddInScrollPane(mergedNodes);
+    }
+    
+    protected JScrollPane buildTheirNodesTable() {
+        theirNodes  = new JTable(
+            model.getTheirNodesTableModel(),
+            new NodeListColumnModel(
+                new NodeListTableCellRenderer()
+            ),
+            model.getTheirNodesSelectionModel()
+         );
+        theirNodes.setName("table.theirnodes");
+        return embeddInScrollPane(theirNodes);
+    }
+    
+    protected void wireActionsToSelectionModels() {
+        myNodes.getSelectionModel().addListSelectionListener(copyStartLeftAction);
+        
+        myNodes.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+        mergedNodes.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+        
+        myNodes.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+        mergedNodes.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+        
+        myNodes.getSelectionModel().addListSelectionListener(copyEndLeftAction);
+        
+        
+        theirNodes.getSelectionModel().addListSelectionListener(copyStartRightAction);
+        
+        theirNodes.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+        mergedNodes.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+        
+        theirNodes.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+        mergedNodes.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+        
+        theirNodes.getSelectionModel().addListSelectionListener(copyEndRightAction);      
+        
+        mergedNodes.getSelectionModel().addListSelectionListener(moveUpMergedAction);
+        mergedNodes.getSelectionModel().addListSelectionListener(moveDownMergedAction);
+        mergedNodes.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);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = 3;
+        gc.weightx = 1.0;
+        freezeAction = new FreezeAction();
+        JToggleButton btn = new JToggleButton(freezeAction);
+        btn.setName("button.freeze");
+        btn.addItemListener(freezeAction);
+        pnl.add(btn, gc);
+        
+        return pnl;
+    }
+        
+    
+    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;
+        JLabel lbl = new JLabel(tr("Nodes in 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;
+        lbl = new JLabel(tr("Nodes in 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(buildMyNodesTable(), 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;
+        add(buildLeftButtonPanel(), 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 = 0.0;
+        add(buildMergedNodesTable(), 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;
+        add(buildRightButtonPanel(), 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 = 0.0;
+        add(buildTheirNodesTable(), gc);
+        
+        gc.gridx = 2;
+        gc.gridy = 2;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.3;
+        gc.weighty = 0.0;
+        add(buildMergedListControlButtons(), gc);
+                
+        wireActionsToSelectionModels();
+    }
+        
+    public NodeListMerger() {
+        model = new NodeListMergeModel();        
+        build();
+        model.addPropertyChangeListener(this);
+    }
+    
+    public void populate(Way my, Way their) {
+        model.populate(my, their);
+    }
+    
+    /**
+     * 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. 
+     *
+     */  
+    abstract class AbstractNodeManipulationAction extends AbstractAction {
+
+        /**
+         * load an icon given by iconName 
+         * 
+         * @param iconName  the name of the icon (without path, i.e. <tt>copystartleft.png</tt>
+         * @return the icon; null, if the icon was not found 
+         */
+        protected ImageIcon getIcon(String iconName) {
+            String fullIconName  = "/images/dialogs/conflict/" + iconName;
+            URL imageURL   = this.getClass().getResource(fullIconName);            
+            if (imageURL == null) {
+                System.out.println(tr("WARNING: failed to load resource {0}", fullIconName));
+                return null;
+            }
+            return new ImageIcon(imageURL);
+        }
+    }
+
+    /**
+     * 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 AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyStartLeftAction() {            
+            ImageIcon icon = getIcon("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);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = myNodes.getSelectedRows();
+            model.copyMyNodesToTop(rows);            
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myNodes.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 AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyEndLeftAction() {            
+            ImageIcon icon = getIcon("copyendleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("> bottom"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes to the end of the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = myNodes.getSelectedRows();
+            model.copyMyNodesToEnd(rows);  
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myNodes.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 AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyBeforeCurrentLeftAction() {            
+            ImageIcon icon = getIcon("copybeforecurrentleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "> before");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes before the first selected node in the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = myNodes.getSelectedRows();
+            int [] mergedRows = mergedNodes.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0) {
+                return;
+            }
+            int current = mergedRows[0];            
+            model.copyMyNodesBeforeCurrent(myRows, current);            
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {        
+            setEnabled(
+                    !myNodes.getSelectionModel().isSelectionEmpty()
+                  && ! mergedNodes.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 AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyAfterCurrentLeftAction() {            
+            ImageIcon icon = getIcon("copyaftercurrentleft.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "> after");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes after the first selected node in the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = myNodes.getSelectedRows();
+            int [] mergedRows = mergedNodes.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0) {
+                return;
+            }
+            int current = mergedRows[0];            
+            model.copyMyNodesAfterCurrent(myRows, current);                        
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {        
+            setEnabled(
+                    !myNodes.getSelectionModel().isSelectionEmpty()
+                  && ! mergedNodes.getSelectionModel().isSelectionEmpty()
+            );            
+        }
+    }
+    
+    
+    class CopyStartRightAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyStartRightAction() {            
+            ImageIcon icon = getIcon("copystartright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< top");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected nodes to the start of the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = theirNodes.getSelectedRows();
+            model.copyTheirNodesToTop(rows);                        
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirNodes.getSelectionModel().isSelectionEmpty());            
+        }
+    }
+    
+    
+    class CopyEndRightAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyEndRightAction() {            
+            ImageIcon icon = getIcon("copyendright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< bottom");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected nodes to the end of the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = theirNodes.getSelectedRows();
+            model.copyTheirNodesToEnd(rows);  
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirNodes.getSelectionModel().isSelectionEmpty());            
+        }
+    }
+    
+    class CopyBeforeCurrentRightAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyBeforeCurrentRightAction() {            
+            ImageIcon icon = getIcon("copybeforecurrentright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< before");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected nodes before the first selected node in the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = theirNodes.getSelectedRows();
+            int [] mergedRows = mergedNodes.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0) {
+                return;
+            }
+            int current = mergedRows[0];            
+            model.copyTheirNodesBeforeCurrent(myRows, current);            
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {        
+            setEnabled(
+                    !theirNodes.getSelectionModel().isSelectionEmpty()
+                  && ! mergedNodes.getSelectionModel().isSelectionEmpty()
+            );            
+        }
+    }
+    
+    
+    class CopyAfterCurrentRightAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public CopyAfterCurrentRightAction() {            
+            ImageIcon icon = getIcon("copyaftercurrentright.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, "< after");
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected nodes after the first selected node in the merged node list"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] myRows = theirNodes.getSelectedRows();
+            int [] mergedRows = mergedNodes.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0) {
+                return;
+            }
+            int current = mergedRows[0];            
+            model.copyTheirNodesAfterCurrent(myRows, current);                        
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {        
+            setEnabled(
+                    !theirNodes.getSelectionModel().isSelectionEmpty()
+                  && ! mergedNodes.getSelectionModel().isSelectionEmpty()
+            );            
+        }
+    }
+    
+    
+    class MoveUpMergedAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public MoveUpMergedAction() {            
+            ImageIcon icon = getIcon("moveup.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Up"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected nodes by one position"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedNodes.getSelectedRows();
+            model.moveUpMergedNodes(rows);            
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedNodes.getSelectedRows();
+            setEnabled(
+                    rows != null
+                  && rows.length > 0
+                  && rows[0] != 0
+            );            
+        }
+    }
+    
+    class MoveDownMergedAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public MoveDownMergedAction() {            
+            ImageIcon icon = getIcon("movedown.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Down"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected nodes by one position"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedNodes.getSelectedRows();
+            model.moveDownMergedNodes(rows);                        
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedNodes.getSelectedRows();
+            setEnabled(
+                    rows != null
+                  && rows.length > 0
+                  && rows[rows.length -1] != mergedNodes.getRowCount() -1
+            );            
+        }
+    }
+    
+    class RemoveMergedAction extends AbstractNodeManipulationAction implements ListSelectionListener {
+
+        public RemoveMergedAction() {            
+            ImageIcon icon = getIcon("remove.png");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Remove"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected nodes from the list of merged nodes"));
+            setEnabled(false);
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedNodes.getSelectedRows();
+            model.removeMergedNodes(rows);                        
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int [] rows = mergedNodes.getSelectedRows();
+            setEnabled(
+                    rows != null
+                  && rows.length > 0
+            );            
+        }
+    }
+    
+    class FreezeAction extends AbstractNodeManipulationAction implements ItemListener  {
+
+        public FreezeAction() {            
+            // FIXME 
+//            ImageIcon icon = getIcon("remove.png");
+//            putValue(Action.SMALL_ICON, icon);
+//            if (icon == null) {
+//                putValue(Action.NAME, tr("Remove"));
+//            }
+            putValue(Action.NAME, tr("Freeze"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged nodes."));
+            putValue(Action.SELECTED_KEY, false);
+            setEnabled(true);
+            
+        }
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int [] rows = mergedNodes.getSelectedRows();
+            model.removeMergedNodes(rows);                        
+        }
+        
+        @Override
+        public void itemStateChanged(ItemEvent e) {
+            int state = e.getStateChange();
+            if (state == ItemEvent.SELECTED) {
+                model.setFrozen(true);
+                putValue(Action.NAME, tr("Unfreeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged nodes and start merging"));
+            } else if (state == ItemEvent.DESELECTED) {
+                model.setFrozen(false);
+                putValue(Action.NAME, tr("Freeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged nodes"));
+            }            
+        }  
+    }
+
+    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
+        myNodes.getSelectionModel().clearSelection();
+        myNodes.setEnabled(!newValue);        
+        theirNodes.getSelectionModel().clearSelection();
+        theirNodes.setEnabled(!newValue);
+        mergedNodes.getSelectionModel().clearSelection();
+        mergedNodes.setEnabled(!newValue);
+        freezeAction.putValue(Action.SELECTED_KEY, newValue);
+    }
+    
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(NodeListMergeModel.PROP_FROZEN)) {
+            handlePropertyChangeFrozen((Boolean)evt.getOldValue(), (Boolean)evt.getNewValue());
+        }
+        
+    }
+    
+    public NodeListMergeModel getModel() {
+        return model;
+    }
+    
+    
+    
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListTableCellRenderer.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/nodes/NodeListTableCellRenderer.java	(revision 1622)
@@ -0,0 +1,107 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.nodes;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.net.URL;
+import java.text.DecimalFormat;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.table.TableCellRenderer;
+
+import org.openstreetmap.josm.data.osm.Node;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * This is the {@see TableCellRenderer} used in the node tables of {@see NodeListMerger}.
+ * 
+ *
+ */
+public  class NodeListTableCellRenderer extends JLabel implements TableCellRenderer {
+    private static DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000");
+    public final static Color BGCOLOR_SELECTED = new Color(143,170,255);
+
+    /**
+     * Load the image icon for an OSM primitive of type node 
+     * 
+     * @return the icon; null, if not found 
+     */
+    protected ImageIcon loadIcon() {
+        URL url = this.getClass().getResource("/images/data/node.png");;
+        if (url == null) {
+            System.out.println(tr("Failed to load resource /images/data/node.png"));
+            return null;
+        }
+        return new ImageIcon(url);
+    }
+    
+    /**
+     * constructor 
+     */
+    public NodeListTableCellRenderer() {
+        setIcon(loadIcon());
+        setOpaque(true);
+    }
+    
+    /**
+     * creates the display name for a node. The name is derived from the nodes id,
+     * its name (i.e. the value of the tag with key name) and its coordinates.
+     *  
+     * @param node  the node 
+     * @return the display name 
+     */
+    protected String getDisplayName(Node node) {
+       StringBuilder sb = new StringBuilder();
+       if (node.get("name") != null) {
+           sb.append(node.get("name"));
+           sb.append("/");
+           sb.append(node.id);
+       } else {
+           sb.append(node.id);
+       }
+       sb.append(" (");
+        
+       if (node.coor != null) {
+           sb.append(COORD_FORMATTER.format(node.coor.lat()));
+           sb.append(",");
+           sb.append(COORD_FORMATTER.format(node.coor.lon()));
+       } else {
+           sb.append("?,?");
+       }
+       sb.append(")");
+       return sb.toString();
+    }
+    
+    /**
+     * reset the renderer 
+     */
+    protected void reset() {
+        setBackground(Color.WHITE);
+        setForeground(Color.BLACK);
+    }
+    
+    /**
+     * render a node 
+     * @param node the node 
+     * @param isSelected
+     */
+    protected  void renderNode(Node node, boolean isSelected) {
+        if (isSelected) {
+            setBackground(BGCOLOR_SELECTED);
+        } 
+        setText(getDisplayName(node));
+    }
+    
+    @Override
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
+            int row, int column) {
+        
+        Node node = (Node)value;
+        reset();
+        renderNode(node,isSelected);
+        return this;
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MergeDecisionType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MergeDecisionType.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MergeDecisionType.java	(revision 1622)
@@ -0,0 +1,8 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+public enum MergeDecisionType {
+    KEEP_MINE,
+    KEEP_THEIR,
+    UNDECIDED,
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MineTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MineTableCellRenderer.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MineTableCellRenderer.java	(revision 1622)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import java.awt.Color;
+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/tags/TagMergeColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeColumnModel.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeColumnModel.java	(revision 1622)
@@ -0,0 +1,34 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.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/tags/TagMergeItem.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeItem.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeItem.java	(revision 1622)
@@ -0,0 +1,133 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.HashMap;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+/**
+ * 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 'key' must not be null"));
+        }
+        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 'key' must not be null"));
+        if (my == null) throw new IllegalArgumentException(tr("parameter 'my' must not be null"));
+        if (their == null) throw new IllegalArgumentException(tr("parameter 'their' must not be null"));
+        this.key = key;
+        myTagValue = null;
+        if (my.keys != null && my.keys.containsKey(key)) {
+            myTagValue = my.keys.get(key);
+        } 
+        theirTagValue = null;
+        if (their.keys != null && their.keys.containsKey(key)) {
+            theirTagValue = their.keys.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("argument 'decision' must not be null"));
+        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 'primitive' must not be null"));
+        if (mergeDecision == MergeDecisionType.UNDECIDED) {
+            throw new IllegalStateException(tr("cannot apply undecided tag merge item"));
+        } else if (mergeDecision == MergeDecisionType.KEEP_THEIR) {
+            if (theirTagValue == null && primitive.keys != null) {
+                primitive.keys.remove(key);
+            } else if (theirTagValue != null) {
+                if (primitive.keys == null) {
+                    primitive.keys = new HashMap<String, String>();
+                }
+                primitive.keys.put(key, theirTagValue);
+            }
+        } else if (mergeDecision == MergeDecisionType.KEEP_MINE) {
+            if (myTagValue == null && primitive.keys != null) {
+                primitive.keys.remove(key);
+            } else if (myTagValue != null) {
+                if (primitive.keys == null) {
+                    primitive.keys = new HashMap<String, String>();
+                }
+                primitive.keys.put(key, myTagValue);
+            } 
+        } else {
+           // should not happen
+        }
+    }
+    
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeModel.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeModel.java	(revision 1622)
@@ -0,0 +1,207 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.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;
+
+/**
+ * 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 ArrayList<TagMergeItem> tagMergeItems;
+    
+    /** the property change listeners */
+    private 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++;
+            }
+        }
+        if (newValue != numUndecidedTags) {
+            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; 
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeTableCellRenderer.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMergeTableCellRenderer.java	(revision 1622)
@@ -0,0 +1,45 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.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);
+    }
+    
+    
+    @Override
+    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 " + col));
+        }
+        return this;
+    }
+    
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMerger.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMerger.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagMerger.java	(revision 1622)
@@ -0,0 +1,369 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Adjustable;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+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.net.URL;
+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;
+
+public class TagMerger extends JPanel {
+
+    private JTable mineTable;
+    private JTable undecidedTable;
+    private JTable theirTable;
+    private TagMergeModel model; 
+    private JButton btnKeepMine;
+    private JButton btnKeepTheir;
+    AdjustmentSynchronizer adjustmentSynchronizer;
+
+    
+    
+    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;
+    }
+    
+    protected JScrollPane buildMineTagTable() {
+        mineTable  = new JTable(
+           model,
+           new TagMergeColumnModel(
+              new MineTableCellRenderer()
+           )
+        );
+        return embeddInScrollPane(mineTable);
+    }
+
+    protected JScrollPane buildTheirTable() {
+        theirTable  = new JTable(
+            model,
+            new TagMergeColumnModel(
+               new TheirTableCellRenderer()
+            )
+         );
+         return embeddInScrollPane(theirTable);
+    }
+    
+    
+    protected JScrollPane buildUndecidedTable() {
+        undecidedTable  = new JTable(
+            model,
+            new TagMergeColumnModel(
+               new UndecidedTableCellRenderer()
+            )
+         );
+         return embeddInScrollPane(undecidedTable);
+    }
+    
+    
+    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;
+        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;
+        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);
+        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(buildUndecidedTable(), 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);
+        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();
+        undecidedTable.getSelectionModel().addListSelectionListener(undecidedAction);
+        JButton btnUndecide = new JButton(undecidedAction);
+        add(btnUndecide, gc);
+        
+    }
+    
+    
+    public TagMerger() {
+        model = new TagMergeModel();
+        build();
+    }
+    
+    
+    public TagMergeModel getModel() {
+        return model;
+    }
+    
+    
+    protected ImageIcon loadIcon(String name) {
+       String path = "/images/dialogs/conflict/" + name;
+       URL url = this.getClass().getResource(path);
+       if (url == null) {
+           System.out.println(tr("WARNING: failed to load resource {0}", path));
+           return null;
+       }
+       return new ImageIcon(url);
+    }
+    
+    class KeepMineAction extends AbstractAction implements ListSelectionListener {
+
+       
+        public KeepMineAction() {
+            ImageIcon icon = loadIcon("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);
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = mineTable.getSelectedRows();
+            if (rows == null || rows.length == 0) {
+                return; 
+            }
+            model.decide(rows, MergeDecisionType.KEEP_MINE);     
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+           setEnabled(mineTable.getSelectedRowCount() > 0);            
+        }
+    }
+    
+    class KeepTheirAction extends AbstractAction implements ListSelectionListener {
+        
+        public KeepTheirAction() {
+            ImageIcon icon = loadIcon("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);
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = theirTable.getSelectedRows();
+            if (rows == null || rows.length == 0) {
+                return; 
+            }
+            model.decide(rows, MergeDecisionType.KEEP_THEIR);     
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+           setEnabled(theirTable.getSelectedRowCount() > 0);            
+        }
+    }
+    
+    
+    class AdjustmentSynchronizer implements AdjustmentListener {
+        private 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);
+        }
+
+        @Override
+        public void adjustmentValueChanged(AdjustmentEvent e) {
+            for (Adjustable a : synchronizedAdjustables) {
+                if (a != e.getAdjustable()) {
+                    a.setValue(e.getValue());
+                }
+            }
+        }
+    }
+    
+    
+    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 = (JTable)mineTable;
+                mergeDecision = MergeDecisionType.KEEP_MINE;
+            } else if (e.getSource() == theirTable) {
+                table = (JTable)theirTable;
+                mergeDecision = MergeDecisionType.KEEP_THEIR;
+            } else if (e.getSource() == undecidedTable) {
+                table = (JTable)undecidedTable;
+                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);
+        }
+    } 
+    
+
+
+    
+    class UndecideAction extends AbstractAction implements ListSelectionListener  {
+
+        public UndecideAction() {
+            ImageIcon icon = loadIcon("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);
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int rows[] = undecidedTable.getSelectedRows();
+            if (rows == null || rows.length == 0) {
+                return; 
+            }
+            model.decide(rows, MergeDecisionType.UNDECIDED);     
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+           setEnabled(undecidedTable.getSelectedRowCount() > 0);            
+        }    
+    }
+    
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TheirTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TheirTableCellRenderer.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TheirTableCellRenderer.java	(revision 1622)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+
+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/conflict/tags/UndecidedTableCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/UndecidedTableCellRenderer.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/UndecidedTableCellRenderer.java	(revision 1622)
@@ -0,0 +1,72 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+
+public class UndecidedTableCellRenderer 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/dialogs/ConflictDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictDialog.java	(revision 1621)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictDialog.java	(revision 1622)
@@ -107,4 +107,39 @@
 
     private final void resolve() {
+        String method = Main.pref.get("conflict.resolution", "traditional");
+        method = method.trim().toLowerCase();
+        if (method.equals("traditional")) {
+            resolveTraditional();            
+        } else if (method.equals("extended")) {
+            resolveExtended();
+        } else {
+            System.out.println(tr("WARNING: unexpected value for preference conflict.resolution, got " + method));
+            resolveTraditional();
+        }
+    }
+    
+    
+    private final void resolveExtended() {
+        if(model.size() == 1)
+            displaylist.setSelectedIndex(0);
+        
+        if (displaylist.getSelectedIndex() == -1)
+            return;
+        
+        int [] selectedRows = displaylist.getSelectedIndices();
+        if (selectedRows == null || selectedRows.length == 0) {
+            return; 
+        }
+        int row = selectedRows[0];
+        OsmPrimitive my = (OsmPrimitive)model.get(row);
+        OsmPrimitive their = conflicts.get(my);
+        ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent);      
+        dialog.getConflictResolver().populate(my, their);
+        dialog.setVisible(true);
+        Main.map.mapView.repaint();
+    }
+    
+    
+    private final void resolveTraditional() {
         if(model.size() == 1)
             displaylist.setSelectedIndex(0);
@@ -133,14 +168,8 @@
     public final void rebuildList() {
         model.removeAllElements();
-        for (OsmPrimitive osm : this.conflicts.keySet())
-            if (osm instanceof Node)
-                model.addElement(osm);
-        for (OsmPrimitive osm : this.conflicts.keySet())
-            if (osm instanceof Way)
-                model.addElement(osm);
-        for (OsmPrimitive osm : this.conflicts.keySet())
-            if (osm instanceof Relation)
-                model.addElement(osm);
-
+        for (OsmPrimitive osm : this.conflicts.keySet()) {
+            model.addElement(osm);
+        }
+        
         if(model.size() != 0) {
             setTitle(tr("Conflicts: {0}", model.size()), true);
@@ -197,6 +226,10 @@
             }
         };
-        for (Object o : displaylist.getSelectedValues())
+        for (Object o : displaylist.getSelectedValues()) {
+            if (conflicts.get(o) == null) {
+                continue;
+            }
             conflicts.get(o).visit(conflictPainter);
+        }
     }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java	(revision 1622)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictResolutionDialog.java	(revision 1622)
@@ -0,0 +1,164 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.net.URL;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.gui.conflict.ConflictResolver;
+
+public class ConflictResolutionDialog extends JDialog {
+    private static final Logger logger = Logger.getLogger(ConflictResolutionDialog.class.getName());
+    public final static Dimension DEFAULT_SIZE = new Dimension(600,400);
+
+    private ConflictResolver resolver;
+        
+    protected void restorePositionAndDimension() {
+        Point p = new Point();
+        Dimension d = new Dimension();
+        try {
+            p.x = Integer.parseInt(Main.pref.get("conflictresolutiondialog.x", "0"));
+            p.x = Math.max(0,p.x);
+        } catch(Exception e) {
+            logger.warning("unexpected value for preference conflictresolutiondialog.x, assuming 0"); 
+            p.x = 0;
+        }
+        try {
+            p.y = Integer.parseInt(Main.pref.get("conflictresolutiondialog.y", "0"));
+            p.y = Math.max(0,p.y);
+        } catch(Exception e) {
+            logger.warning("unexpected value for preference conflictresolutiondialog.x, assuming 0"); 
+            p.y = 0;
+        }
+        try {
+            d.width = Integer.parseInt(Main.pref.get("conflictresolutiondialog.width", Integer.toString(DEFAULT_SIZE.width)));
+            d.width = Math.max(0,d.width);
+        } catch(Exception e) {
+            logger.warning("unexpected value for preference conflictresolutiondialog.width, assuming " + DEFAULT_SIZE.width); 
+            p.y = 0;
+        }
+        try {
+            d.height = Integer.parseInt(Main.pref.get("conflictresolutiondialog.height", Integer.toString(DEFAULT_SIZE.height)));
+            d.height = Math.max(0,d.height);
+        } catch(Exception e) {
+            logger.warning("unexpected value for preference conflictresolutiondialog.height, assuming " +  + DEFAULT_SIZE.height); 
+            p.y = 0;
+        }
+        
+        setLocation(p);
+        setSize(d);
+    }
+    
+    protected void rememberPositionAndDimension() {
+        Point p = getLocation();
+        Main.pref.put("conflictresolutiondialog.x", Integer.toString(p.x));
+        Main.pref.put("conflictresolutiondialog.y", Integer.toString(p.y));
+        
+        Dimension d = getSize();
+        Main.pref.put("conflictresolutiondialog.width", Integer.toString(d.width));
+        Main.pref.put("conflictresolutiondialog.height", Integer.toString(d.height));
+    }
+    
+    public void setVisible(boolean isVisible) {
+        if (isVisible){
+            restorePositionAndDimension();
+        } else {
+            rememberPositionAndDimension();
+        }
+        super.setVisible(isVisible);
+    }
+    
+    protected JPanel buildButtonRow() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new FlowLayout(FlowLayout.RIGHT));
+        
+        JButton btn = new JButton(new CancelAction());
+        btn.setName("button.cancel");
+        pnl.add(btn);
+        
+        btn = new JButton(new ApplyResolutionAction());
+        btn.setName("button.apply");
+        pnl.add(btn);
+        
+        pnl.setBorder(BorderFactory.createLoweredBevelBorder());
+        return pnl;
+    }
+    
+    protected void build() {
+       setTitle(tr("Resolve conflicts"));
+       getContentPane().setLayout(new BorderLayout());     
+       
+       resolver = new ConflictResolver();
+       getContentPane().add(resolver, BorderLayout.CENTER);       
+       getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);       
+    }
+    
+    
+    public ConflictResolutionDialog(Component parent) {
+        super(JOptionPane.getFrameForComponent(parent), true /* modal */);
+        build();
+    }
+    
+    public ConflictResolver getConflictResolver() {
+        return resolver;
+    }
+    
+    protected ImageIcon getIcon(String iconPath) {
+        URL imageURL   = this.getClass().getResource(iconPath);            
+        if (imageURL == null) {
+            System.out.println(tr("WARNING: failed to load resource {0}", iconPath));
+            return null;
+        }
+        return new ImageIcon(imageURL);
+    }
+
+    
+    class CancelAction extends AbstractAction {
+        
+        public CancelAction() {            
+            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution and close the dialog"));
+            putValue(Action.NAME, tr("Cancel"));
+            putValue(Action.SMALL_ICON, getIcon("/images/cancel.png"));
+            setEnabled(true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            setVisible(false);
+        }        
+    }   
+    
+    class ApplyResolutionAction extends AbstractAction {        
+        public ApplyResolutionAction() {
+            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts and close the dialog"));
+            putValue(Action.NAME, tr("Apply Resolution"));
+            putValue(Action.SMALL_ICON, getIcon("/images/dialogs/conflict.png"));
+            setEnabled(true);            
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            Command cmd = resolver.buildResolveCommand();
+            Main.main.undoRedo.add(cmd);
+            setVisible(false);
+        }        
+    }
+}
