// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.pair.tags;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Adjustable;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* UI component for resolving conflicts in the tag sets of two {@see OsmPrimitive}s.
*
*/
public class TagMerger extends JPanel implements IConflictResolver {
private JTable mineTable;
private JTable mergedTable;
private JTable theirTable;
private final TagMergeModel model;
private JButton btnKeepMine;
private JButton btnKeepTheir;
AdjustmentSynchronizer adjustmentSynchronizer;
/**
* embeds table in a new {@see JScrollPane} and returns th scroll pane
*
* @param table the table
* @return the scroll pane embedding the table
*/
protected JScrollPane embeddInScrollPane(JTable table) {
JScrollPane pane = new JScrollPane(table);
pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
return pane;
}
/**
* builds the table for my tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildMineTagTable() {
mineTable = new JTable(
model,
new TagMergeColumnModel(
new MineTableCellRenderer()
)
);
mineTable.setName("table.my");
return embeddInScrollPane(mineTable);
}
/**
* builds the table for their tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildTheirTable() {
theirTable = new JTable(
model,
new TagMergeColumnModel(
new TheirTableCellRenderer()
)
);
theirTable.setName("table.their");
return embeddInScrollPane(theirTable);
}
/**
* builds the table for the merged tag set (table already embedded in a scroll pane)
*
* @return the table (embedded in a scroll pane)
*/
protected JScrollPane buildMergedTable() {
mergedTable = new JTable(
model,
new TagMergeColumnModel(
new MergedTableCellRenderer()
)
);
mergedTable.setName("table.merged");
return embeddInScrollPane(mergedTable);
}
/**
* build the user interface
*/
protected void build() {
GridBagConstraints gc = new GridBagConstraints();
setLayout(new GridBagLayout());
adjustmentSynchronizer = new AdjustmentSynchronizer();
gc.gridx = 0;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
gc.insets = new Insets(10,0,10,0);
JLabel lbl = new JLabel(tr("My version (local dataset)"));
add(lbl, gc);
gc.gridx = 2;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
lbl = new JLabel(tr("Merged version"));
add(lbl, gc);
gc.gridx = 4;
gc.gridy = 0;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
gc.insets = new Insets(0,0,0,0);
lbl = new JLabel(tr("Their version (server dataset)"));
add(lbl, gc);
gc.gridx = 0;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
add(buildMineTagTable(), gc);
gc.gridx = 1;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
KeepMineAction keepMineAction = new KeepMineAction();
mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
btnKeepMine = new JButton(keepMineAction);
btnKeepMine.setName("button.keepmine");
add(btnKeepMine, gc);
gc.gridx = 2;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
add(buildMergedTable(), gc);
gc.gridx = 3;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
KeepTheirAction keepTheirAction = new KeepTheirAction();
btnKeepTheir = new JButton(keepTheirAction);
btnKeepTheir.setName("button.keeptheir");
add(btnKeepTheir, gc);
gc.gridx = 4;
gc.gridy = 1;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.BOTH;
gc.anchor = GridBagConstraints.FIRST_LINE_START;
gc.weightx = 0.3;
gc.weighty = 1.0;
add(buildTheirTable(), gc);
theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
mineTable.addMouseListener(dblClickAdapter);
theirTable.addMouseListener(dblClickAdapter);
gc.gridx = 2;
gc.gridy = 2;
gc.gridwidth = 1;
gc.gridheight = 1;
gc.fill = GridBagConstraints.NONE;
gc.anchor = GridBagConstraints.CENTER;
gc.weightx = 0.0;
gc.weighty = 0.0;
UndecideAction undecidedAction = new UndecideAction();
mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
JButton btnUndecide = new JButton(undecidedAction);
btnUndecide.setName("button.undecide");
add(btnUndecide, gc);
}
public TagMerger() {
model = new TagMergeModel();
build();
}
/**
* replies the model used by this tag merger
*
* @return the model
*/
public TagMergeModel getModel() {
return model;
}
private void selectNextConflict(int[] rows) {
int max = rows[0];
for (int row: rows) {
if (row > max) {
max = row;
}
}
int index = model.getFirstUndecided(max+1);
if (index == -1) {
index = model.getFirstUndecided(0);
}
mineTable.getSelectionModel().setSelectionInterval(index, index);
theirTable.getSelectionModel().setSelectionInterval(index, index);
}
/**
* Keeps the currently selected tags in my table in the list of merged tags.
*
*/
class KeepMineAction extends AbstractAction implements ListSelectionListener {
public KeepMineAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeepmine.png");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, ">");
}
putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
setEnabled(false);
}
public void actionPerformed(ActionEvent arg0) {
int rows[] = mineTable.getSelectedRows();
if (rows == null || rows.length == 0)
return;
model.decide(rows, MergeDecisionType.KEEP_MINE);
selectNextConflict(rows);
}
public void valueChanged(ListSelectionEvent e) {
setEnabled(mineTable.getSelectedRowCount() > 0);
}
}
/**
* Keeps the currently selected tags in their table in the list of merged tags.
*
*/
class KeepTheirAction extends AbstractAction implements ListSelectionListener {
public KeepTheirAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeeptheir.png");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, ">");
}
putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
setEnabled(false);
}
public void actionPerformed(ActionEvent arg0) {
int rows[] = theirTable.getSelectedRows();
if (rows == null || rows.length == 0)
return;
model.decide(rows, MergeDecisionType.KEEP_THEIR);
selectNextConflict(rows);
}
public void valueChanged(ListSelectionEvent e) {
setEnabled(theirTable.getSelectedRowCount() > 0);
}
}
/**
* Synchronizes scrollbar adjustments between a set of
* {@see Adjustable}s. Whenever the adjustment of one of
* the registerd Adjustables is updated the adjustment of
* the other registered Adjustables is adjusted too.
*
*/
static class AdjustmentSynchronizer implements AdjustmentListener {
private final ArrayList<Adjustable> synchronizedAdjustables;
public AdjustmentSynchronizer() {
synchronizedAdjustables = new ArrayList<Adjustable>();
}
public void synchronizeAdjustment(Adjustable adjustable) {
if (adjustable == null)
return;
if (synchronizedAdjustables.contains(adjustable))
return;
synchronizedAdjustables.add(adjustable);
adjustable.addAdjustmentListener(this);
}
public void adjustmentValueChanged(AdjustmentEvent e) {
for (Adjustable a : synchronizedAdjustables) {
if (a != e.getAdjustable()) {
a.setValue(e.getValue());
}
}
}
}
/**
* Handler for double clicks on entries in the three tag tables.
*
*/
class DoubleClickAdapter extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() != 2)
return;
JTable table = null;
MergeDecisionType mergeDecision;
if (e.getSource() == mineTable) {
table = mineTable;
mergeDecision = MergeDecisionType.KEEP_MINE;
} else if (e.getSource() == theirTable) {
table = theirTable;
mergeDecision = MergeDecisionType.KEEP_THEIR;
} else if (e.getSource() == mergedTable) {
table = mergedTable;
mergeDecision = MergeDecisionType.UNDECIDED;
} else
// double click in another component; shouldn't happen,
// but just in case
return;
int row = table.rowAtPoint(e.getPoint());
model.decide(row, mergeDecision);
}
}
/**
* Sets the currently selected tags in the table of merged tags to state
* {@see MergeDecisionType#UNDECIDED}
*
*/
class UndecideAction extends AbstractAction implements ListSelectionListener {
public UndecideAction() {
ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagundecide.png");
if (icon != null) {
putValue(Action.SMALL_ICON, icon);
putValue(Action.NAME, "");
} else {
putValue(Action.NAME, tr("Undecide"));
}
putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
setEnabled(false);
}
public void actionPerformed(ActionEvent arg0) {
int rows[] = mergedTable.getSelectedRows();
if (rows == null || rows.length == 0)
return;
model.decide(rows, MergeDecisionType.UNDECIDED);
}
public void valueChanged(ListSelectionEvent e) {
setEnabled(mergedTable.getSelectedRowCount() > 0);
}
}
public void deletePrimitive(boolean deleted) {
// Use my entries, as it doesn't really matter
MergeDecisionType decision = deleted?MergeDecisionType.KEEP_MINE:MergeDecisionType.UNDECIDED;
for (int i=0; i<model.getRowCount(); i++) {
model.decide(i, decision);
}
}
public void populate(Conflict<? extends OsmPrimitive> conflict) {
model.populate(conflict.getMy(), conflict.getTheir());
mineTable.getSelectionModel().setSelectionInterval(0, 0);
theirTable.getSelectionModel().setSelectionInterval(0, 0);
}
}