// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.pair;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Observable;
import java.util.logging.Logger;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
/**
* ListMergeModel is a model for interactively comparing and merging two list of entries
* of type T. It maintains three lists of entries of type T:
*
* - the list of my entries
* - the list of their entries
* - the list of merged entries
*
*
* A ListMergeModel is a factory for three {@see TableModel}s and three {@see ListSelectionModel}s:
*
* - the table model and the list selection for for a {@see JTable} which shows my entries.
* See {@see #getMyTableModel()}
and {@see ListMergeModel#getMySelectionModel()}
* - dito for their entries and merged entries
*
*
* A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
* decisions. {@see PropertyChangeListener}s can register for property value changes of
* {@see #PROP_FROZEN}.
*
* ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
*
* - {@see ListMergeModel#cloneEntryForMergedList(Object)} - clones an entry of type T
* - {@see ListMergeModel#isEqualEntry(Object, Object)} - checks whether two entries are equals
* - {@see ListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
* a JTable, dispatched from {@see TableModel#setValueAt(Object, int, int)}
*
* A ListMergeModel is used in combination with a {@see ListMerger}.
*
* @param the type of the list entries
* @see ListMerger
*/
public abstract class ListMergeModel extends Observable {
//private static final Logger logger = Logger.getLogger(ListMergeModel.class.getName());
public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen";
protected HashMap> entries;
protected DefaultTableModel myEntriesTableModel;
protected DefaultTableModel theirEntriesTableModel;
protected DefaultTableModel mergedEntriesTableModel;
protected EntriesSelectionModel myEntriesSelectionModel;
protected EntriesSelectionModel theirEntriesSelectionModel;
protected EntriesSelectionModel mergedEntriesSelectionModel;
private final ArrayList listeners;
private boolean isFrozen = false;
private final ComparePairListModel comparePairListModel;
/**
* Creates a clone of an entry of type T suitable to be included in the
* list of merged entries
*
* @param entry the entry
* @return the cloned entry
*/
protected abstract T cloneEntryForMergedList(T entry);
/**
* checks whether two entries are equal. This is not necessarily the same as
* e1.equals(e2).
*
* @param e1 the first entry
* @param e2 the second entry
* @return true, if the entries are equal, false otherwise.
*/
public abstract boolean isEqualEntry(T e1, T e2);
/**
* Handles method dispatches from {@see TableModel#setValueAt(Object, int, int)}.
*
* @param model the table model
* @param value the value to be set
* @param row the row index
* @param col the column index
*
* @see TableModel#setValueAt(Object, int, int)
*/
protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
protected void buildMyEntriesTableModel() {
myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
}
protected void buildTheirEntriesTableModel() {
theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
}
protected void buildMergedEntriesTableModel() {
mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
}
protected ArrayList getMergedEntries() {
return entries.get(MERGED_ENTRIES);
}
protected ArrayList getMyEntries() {
return entries.get(MY_ENTRIES);
}
protected ArrayList getTheirEntries() {
return entries.get(THEIR_ENTRIES);
}
public int getMyEntriesSize() {
return getMyEntries().size();
}
public int getMergedEntriesSize() {
return getMergedEntries().size();
}
public int getTheirEntriesSize() {
return getTheirEntries().size();
}
public ListMergeModel() {
entries = new HashMap>();
for (ListRole role : ListRole.values()) {
entries.put(role, new ArrayList());
}
buildMyEntriesTableModel();
buildTheirEntriesTableModel();
buildMergedEntriesTableModel();
myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
listeners = new ArrayList();
comparePairListModel = new ComparePairListModel();
setFrozen(true);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
synchronized(listeners) {
if (listener != null && ! listeners.contains(listener)) {
listeners.add(listener);
}
}
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
synchronized(listeners) {
if (listener != null && listeners.contains(listener)) {
listeners.remove(listener);
}
}
}
protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
synchronized(listeners) {
PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
for (PropertyChangeListener listener: listeners) {
listener.propertyChange(evt);
}
}
}
public void setFrozen(boolean isFrozen) {
boolean oldValue = this.isFrozen;
this.isFrozen = isFrozen;
fireFrozenChanged(oldValue, this.isFrozen);
}
public boolean isFrozen() {
return isFrozen;
}
public TableModel getMyTableModel() {
return myEntriesTableModel;
}
public TableModel getTheirTableModel() {
return theirEntriesTableModel;
}
public TableModel getMergedTableModel() {
return mergedEntriesTableModel;
}
public EntriesSelectionModel getMySelectionModel() {
return myEntriesSelectionModel;
}
public EntriesSelectionModel getTheirSelectionModel() {
return theirEntriesSelectionModel;
}
public EntriesSelectionModel getMergedSelectionModel() {
return mergedEntriesSelectionModel;
}
protected void fireModelDataChanged() {
myEntriesTableModel.fireTableDataChanged();
theirEntriesTableModel.fireTableDataChanged();
mergedEntriesTableModel.fireTableDataChanged();
setChanged();
notifyObservers();
}
protected void copyToTop(ListRole role, int []rows) {
if (rows == null || rows.length == 0)
return;
for (int i = rows.length - 1; i >= 0; i--) {
int row = rows[i];
T n = entries.get(role).get(row);
entries.get(MERGED_ENTRIES).add(0, cloneEntryForMergedList(n));
}
fireModelDataChanged();
mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes at the top of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyMyToTop(int [] rows) {
copyToTop(MY_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes at the top of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyTheirToTop(int [] rows) {
copyToTop(THEIR_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of nodes in source to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param source the list of nodes to copy from
* @param rows the indices
*/
public void copyToEnd(ListRole source, int [] rows) {
if (rows == null || rows.length == 0)
return;
ArrayList mergedEntries = getMergedEntries();
for (int row : rows) {
T n = entries.get(source).get(row);
mergedEntries.add(cloneEntryForMergedList(n));
}
fireModelDataChanged();
mergedEntriesSelectionModel.setSelectionInterval(mergedEntries.size()-rows.length, mergedEntries.size() -1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyMyToEnd(int [] rows) {
copyToEnd(MY_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes at the end of the list of merged
* nodes.
*
* @param rows the indices
*/
public void copyTheirToEnd(int [] rows) {
copyToEnd(THEIR_ENTRIES, rows);
}
/**
* Copies the nodes given by indices in rows from the list of nodes source
to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param source the list of nodes to copy from
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
protected void copyBeforeCurrent(ListRole source, int [] rows, int current) {
if (rows == null || rows.length == 0)
return;
ArrayList mergedEntries = getMergedEntries();
if (current < 0 || current >= mergedEntries.size())
throw new IllegalArgumentException(tr("Parameter current out of range. Got {0}.", current));
for (int i=rows.length -1; i>=0; i--) {
int row = rows[i];
T n = entries.get(source).get(row);
mergedEntries.add(current, cloneEntryForMergedList(n));
}
fireModelDataChanged();
mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
public void copyMyBeforeCurrent(int [] rows, int current) {
copyBeforeCurrent(MY_ENTRIES,rows,current);
}
/**
* Copies the nodes given by indices in rows from the list of their nodes to the
* list of merged nodes. Inserts the nodes before row given by current.
*
* @param rows the indices
* @param current the row index before which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
public void copyTheirBeforeCurrent(int [] rows, int current) {
copyBeforeCurrent(THEIR_ENTRIES,rows,current);
}
/**
* Copies the nodes given by indices in rows from the list of nodes source
to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param source the list of nodes to copy from
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
protected void copyAfterCurrent(ListRole source, int [] rows, int current) {
if (rows == null || rows.length == 0)
return;
ArrayList mergedEntries = getMergedEntries();
if (current < 0 || current >= mergedEntries.size())
throw new IllegalArgumentException(tr("Parameter current out of range. Got {0}.", current));
if (current == mergedEntries.size() -1) {
copyToEnd(source, rows);
} else {
for (int i=rows.length -1; i>=0; i--) {
int row = rows[i];
T n = entries.get(source).get(row);
mergedEntries.add(current+1, cloneEntryForMergedList(n));
}
}
fireModelDataChanged();
mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
notifyObservers();
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
public void copyMyAfterCurrent(int [] rows, int current) {
copyAfterCurrent(MY_ENTRIES, rows, current);
}
/**
* Copies the nodes given by indices in rows from the list of my nodes to the
* list of merged nodes. Inserts the nodes after the row given by current.
*
* @param rows the indices
* @param current the row index after which the nodes are inserted
* @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
*
*/
public void copyTheirAfterCurrent(int [] rows, int current) {
copyAfterCurrent(THEIR_ENTRIES, rows, current);
}
/**
* Moves the nodes given by indices in rows up by one position in the list
* of merged nodes.
*
* @param rows the indices
*
*/
public void moveUpMerged(int [] rows) {
if (rows == null || rows.length == 0)
return;
if (rows[0] == 0)
// can't move up
return;
ArrayList mergedEntries = getMergedEntries();
for (int row: rows) {
T n = mergedEntries.get(row);
mergedEntries.remove(row);
mergedEntries.add(row -1, n);
}
fireModelDataChanged();
notifyObservers();
mergedEntriesSelectionModel.clearSelection();
for (int row: rows) {
mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
}
}
/**
* Moves the nodes given by indices in rows down by one position in the list
* of merged nodes.
*
* @param rows the indices
*/
public void moveDownMerged(int [] rows) {
if (rows == null || rows.length == 0)
return;
ArrayList mergedEntries = getMergedEntries();
if (rows[rows.length -1] == mergedEntries.size() -1)
// can't move down
return;
for (int i = rows.length-1; i>=0;i--) {
int row = rows[i];
T n = mergedEntries.get(row);
mergedEntries.remove(row);
mergedEntries.add(row +1, n);
}
fireModelDataChanged();
notifyObservers();
mergedEntriesSelectionModel.clearSelection();
for (int row: rows) {
mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
}
}
/**
* Removes the nodes given by indices in rows from the list
* of merged nodes.
*
* @param rows the indices
*/
public void removeMerged(int [] rows) {
if (rows == null || rows.length == 0)
return;
ArrayList mergedEntries = getMergedEntries();
for (int i = rows.length-1; i>=0;i--) {
mergedEntries.remove(rows[i]);
}
fireModelDataChanged();
notifyObservers();
mergedEntriesSelectionModel.clearSelection();
}
/**
* Replies true if the list of my entries and the list of their
* entries are equal
*
* @return true, if the lists are equal; false otherwise
*/
protected boolean myAndTheirEntriesEqual() {
if (getMyEntries().size() != getTheirEntries().size())
return false;
for (int i=0; i < getMyEntries().size(); i++) {
if (! isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
return false;
}
return true;
}
/**
* This an adapter between a {@see JTable} and one of the three entry lists
* in the role {@see ListRole} managed by the {@see ListMergeModel}.
*
* From the point of view of the {@see JTable} it is a {@see TableModel}.
*
* @param
* @see ListMergeModel#getMyTableModel()
* @see ListMergeModel#getTheirTableModel()
* @see ListMergeModel#getMergedTableModel()
*/
public class EntriesTableModel extends DefaultTableModel {
private final ListRole role;
/**
*
* @param role the role
*/
public EntriesTableModel(ListRole role) {
this.role = role;
}
@Override
public int getRowCount() {
int count = Math.max(getMyEntries().size(), getMergedEntries().size());
count = Math.max(count, getTheirEntries().size());
return count;
}
@Override
public Object getValueAt(int row, int column) {
if (row < entries.get(role).size())
return entries.get(role).get(row);
return null;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public void setValueAt(Object value, int row, int col) {
ListMergeModel.this.setValueAt(this, value,row,col);
}
public ListMergeModel getListMergeModel() {
return ListMergeModel.this;
}
/**
* replies true if the {@see ListRole} of this {@see EntriesTableModel}
* participates in the current {@see ComparePairType}
*
* @return true, if the if the {@see ListRole} of this {@see EntriesTableModel}
* participates in the current {@see ComparePairType}
*
* @see ComparePairListModel#getSelectedComparePair()
*/
public boolean isParticipatingInCurrentComparePair() {
return getComparePairListModel()
.getSelectedComparePair()
.isParticipatingIn(role);
}
/**
* replies true if the entry at row
is equal to the entry at the
* same position in the opposite list of the current {@see ComparePairType}.
*
* @param row the row number
* @return true if the entry at row
is equal to the entry at the
* same position in the opposite list of the current {@see ComparePairType}
* @exception IllegalStateException thrown, if this model is not participating in the
* current {@see ComparePairType}
* @see ComparePairType#getOppositeRole(ListRole)
* @see #getRole()
* @see #getOppositeEntries()
*/
public boolean isSamePositionInOppositeList(int row) {
if (!isParticipatingInCurrentComparePair())
throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
if (row >= getEntries().size()) return false;
if (row >= getOppositeEntries().size()) return false;
T e1 = getEntries().get(row);
T e2 = getOppositeEntries().get(row);
return isEqualEntry(e1, e2);
}
/**
* replies true if the entry at the current position is present in the opposite list
* of the current {@see ComparePairType}.
*
* @param row the current row
* @return true if the entry at the current position is present in the opposite list
* of the current {@see ComparePairType}.
* @exception IllegalStateException thrown, if this model is not participating in the
* current {@see ComparePairType}
* @see ComparePairType#getOppositeRole(ListRole)
* @see #getRole()
* @see #getOppositeEntries()
*/
public boolean isIncludedInOppositeList(int row) {
if (!isParticipatingInCurrentComparePair())
throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
if (row >= getEntries().size()) return false;
T e1 = getEntries().get(row);
for (T e2: getOppositeEntries()) {
if (isEqualEntry(e1, e2)) return true;
}
return false;
}
protected ArrayList getEntries() {
return entries.get(role);
}
/**
* replies the opposite list of entries with respect to the current {@see ComparePairType}
*
* @return the opposite list of entries
*/
protected ArrayList getOppositeEntries() {
ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
return entries.get(opposite);
}
public ListRole getRole() {
return role;
}
}
/**
* This is the selection model to be used in a {@see JTable} which displays
* an entry list managed by {@see ListMergeModel}.
*
* The model ensures that only rows displaying an entry in the entry list
* can be selected. "Empty" rows can't be selected.
*
* @see ListMergeModel#getMySelectionModel()
* @see ListMergeModel#getMergedSelectionModel()
* @see ListMergeModel#getTheirSelectionModel()
*
*/
protected class EntriesSelectionModel extends DefaultListSelectionModel {
private final ArrayList entries;
public EntriesSelectionModel(ArrayList nodes) {
this.entries = nodes;
}
@Override
public void addSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
if (index0 > entries.size() - 1) return;
index0 = Math.min(entries.size()-1, index0);
index1 = Math.min(entries.size()-1, index1);
super.addSelectionInterval(index0, index1);
}
@Override
public void insertIndexInterval(int index, int length, boolean before) {
if (entries.isEmpty()) return;
if (before) {
int newindex = Math.min(entries.size()-1, index);
if (newindex < index - length) return;
length = length - (index - newindex);
super.insertIndexInterval(newindex, length, before);
} else {
if (index > entries.size() -1) return;
length = Math.min(entries.size()-1 - index, length);
super.insertIndexInterval(index, length, before);
}
}
@Override
public void moveLeadSelectionIndex(int leadIndex) {
if (entries.isEmpty()) return;
leadIndex = Math.max(0, leadIndex);
leadIndex = Math.min(entries.size() - 1, leadIndex);
super.moveLeadSelectionIndex(leadIndex);
}
@Override
public void removeIndexInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.removeIndexInterval(index0, index1);
}
@Override
public void removeSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.removeSelectionInterval(index0, index1);
}
@Override
public void setAnchorSelectionIndex(int anchorIndex) {
if (entries.isEmpty()) return;
anchorIndex = Math.min(entries.size() - 1, anchorIndex);
super.setAnchorSelectionIndex(anchorIndex);
}
@Override
public void setLeadSelectionIndex(int leadIndex) {
if (entries.isEmpty()) return;
leadIndex = Math.min(entries.size() - 1, leadIndex);
super.setLeadSelectionIndex(leadIndex);
}
@Override
public void setSelectionInterval(int index0, int index1) {
if (entries.isEmpty()) return;
index0 = Math.max(0, index0);
index0 = Math.min(entries.size() - 1, index0);
index1 = Math.max(0, index1);
index1 = Math.min(entries.size() - 1, index1);
super.setSelectionInterval(index0, index1);
}
}
public ComparePairListModel getComparePairListModel() {
return this.comparePairListModel;
}
public class ComparePairListModel extends AbstractListModel implements ComboBoxModel {
private int selectedIdx;
private final ArrayList compareModes;
public ComparePairListModel() {
this.compareModes = new ArrayList();
compareModes.add(MY_WITH_THEIR);
compareModes.add(MY_WITH_MERGED);
compareModes.add(THEIR_WITH_MERGED);
selectedIdx = 0;
}
public Object getElementAt(int index) {
if (index < compareModes.size())
return compareModes.get(index);
throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
}
public int getSize() {
return compareModes.size();
}
public Object getSelectedItem() {
return compareModes.get(selectedIdx);
}
public void setSelectedItem(Object anItem) {
int i = compareModes.indexOf(anItem);
if (i < 0)
throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
selectedIdx = i;
fireModelDataChanged();
}
public ComparePairType getSelectedComparePair() {
return compareModes.get(selectedIdx);
}
}
}