001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair;
003
004import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
005import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
006import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
007import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
008import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
009import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
010import static org.openstreetmap.josm.tools.I18n.tr;
011
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.util.ArrayList;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018import java.util.Observable;
019
020import javax.swing.AbstractListModel;
021import javax.swing.ComboBoxModel;
022import javax.swing.DefaultListSelectionModel;
023import javax.swing.JOptionPane;
024import javax.swing.JTable;
025import javax.swing.ListSelectionModel;
026import javax.swing.table.DefaultTableModel;
027import javax.swing.table.TableModel;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.data.osm.DataSet;
031import org.openstreetmap.josm.data.osm.OsmPrimitive;
032import org.openstreetmap.josm.data.osm.PrimitiveId;
033import org.openstreetmap.josm.data.osm.RelationMember;
034import org.openstreetmap.josm.gui.HelpAwareOptionPane;
035import org.openstreetmap.josm.gui.help.HelpUtil;
036import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
037import org.openstreetmap.josm.tools.CheckParameterUtil;
038
039/**
040 * ListMergeModel is a model for interactively comparing and merging two list of entries
041 * of type T. It maintains three lists of entries of type T:
042 * <ol>
043 *   <li>the list of <em>my</em> entries</li>
044 *   <li>the list of <em>their</em> entries</li>
045 *   <li>the list of <em>merged</em> entries</li>
046 * </ol>
047 *
048 * A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s:
049 * <ol>
050 *   <li>the table model and the list selection for for a  {@link JTable} which shows my entries.
051 *    See {@link #getMyTableModel()}</li> and {@link ListMergeModel#getMySelectionModel()}</li>
052 *   <li>dito for their entries and merged entries</li>
053 * </ol>
054 *
055 * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
056 * decisions. {@link PropertyChangeListener}s can register for property value changes of
057 * {@link #FROZEN_PROP}.
058 *
059 * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
060 * <ul>
061 *   <li>{@link ListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li>
062 *   <li>{@link ListMergeModel#isEqualEntry} - checks whether two entries are equals </li>
063 *   <li>{@link ListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
064 *     a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li>
065 * </ul>
066 * A ListMergeModel is used in combination with a {@link ListMerger}.
067 *
068 * @param <T>  the type of the list entries
069 * @see ListMerger
070 */
071public abstract class ListMergeModel<T extends PrimitiveId> extends Observable {
072    public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen";
073
074    private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5;
075
076    protected Map<ListRole, ArrayList<T>> entries;
077
078    protected EntriesTableModel myEntriesTableModel;
079    protected EntriesTableModel theirEntriesTableModel;
080    protected EntriesTableModel mergedEntriesTableModel;
081
082    protected EntriesSelectionModel myEntriesSelectionModel;
083    protected EntriesSelectionModel theirEntriesSelectionModel;
084    protected EntriesSelectionModel mergedEntriesSelectionModel;
085
086    private final List<PropertyChangeListener> listeners;
087    private boolean isFrozen = false;
088    private final ComparePairListModel comparePairListModel;
089
090    private DataSet myDataset;
091    private Map<PrimitiveId, PrimitiveId> mergedMap;
092
093    /**
094     * Creates a clone of an entry of type T suitable to be included in the
095     * list of merged entries
096     *
097     * @param entry the entry
098     * @return the cloned entry
099     */
100    protected abstract T cloneEntryForMergedList(T entry);
101
102    /**
103     * checks whether two entries are equal. This is not necessarily the same as
104     * e1.equals(e2).
105     *
106     * @param e1  the first entry
107     * @param e2  the second entry
108     * @return true, if the entries are equal, false otherwise.
109     */
110    public abstract boolean isEqualEntry(T e1, T e2);
111
112    /**
113     * Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}.
114     *
115     * @param model the table model
116     * @param value  the value to be set
117     * @param row  the row index
118     * @param col the column index
119     *
120     * @see TableModel#setValueAt(Object, int, int)
121     */
122    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
123
124    /**
125     *
126     * @param entry
127     * @return Primitive from my dataset referenced by entry
128     */
129    public OsmPrimitive getMyPrimitive(T entry) {
130        return getMyPrimitiveById(entry);
131    }
132
133    public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) {
134        OsmPrimitive result = myDataset.getPrimitiveById(entry);
135        if (result == null && mergedMap != null) {
136            PrimitiveId id = mergedMap.get(entry);
137            if (id == null && entry instanceof OsmPrimitive) {
138                id = mergedMap.get(((OsmPrimitive)entry).getPrimitiveId());
139            }
140            if (id != null) {
141                result = myDataset.getPrimitiveById(id);
142            }
143        }
144        return result;
145    }
146
147    protected void buildMyEntriesTableModel() {
148        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
149    }
150
151    protected void buildTheirEntriesTableModel() {
152        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
153    }
154
155    protected void buildMergedEntriesTableModel() {
156        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
157    }
158
159    protected List<T> getMergedEntries() {
160        return entries.get(MERGED_ENTRIES);
161    }
162
163    protected List<T> getMyEntries() {
164        return entries.get(MY_ENTRIES);
165    }
166
167    protected List<T> getTheirEntries() {
168        return entries.get(THEIR_ENTRIES);
169    }
170
171    public int getMyEntriesSize() {
172        return getMyEntries().size();
173    }
174
175    public int getMergedEntriesSize() {
176        return getMergedEntries().size();
177    }
178
179    public int getTheirEntriesSize() {
180        return getTheirEntries().size();
181    }
182
183    public ListMergeModel() {
184        entries = new HashMap<ListRole, ArrayList<T>>();
185        for (ListRole role : ListRole.values()) {
186            entries.put(role, new ArrayList<T>());
187        }
188
189        buildMyEntriesTableModel();
190        buildTheirEntriesTableModel();
191        buildMergedEntriesTableModel();
192
193        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
194        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
195        mergedEntriesSelectionModel =  new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
196
197        listeners = new ArrayList<PropertyChangeListener>();
198        comparePairListModel = new ComparePairListModel();
199
200        setFrozen(true);
201    }
202
203    public void addPropertyChangeListener(PropertyChangeListener listener) {
204        synchronized(listeners) {
205            if (listener != null && ! listeners.contains(listener)) {
206                listeners.add(listener);
207            }
208        }
209    }
210
211    public void removePropertyChangeListener(PropertyChangeListener listener) {
212        synchronized(listeners) {
213            if (listener != null && listeners.contains(listener)) {
214                listeners.remove(listener);
215            }
216        }
217    }
218
219    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
220        synchronized(listeners) {
221            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
222            for (PropertyChangeListener listener: listeners) {
223                listener.propertyChange(evt);
224            }
225        }
226    }
227
228    public void setFrozen(boolean isFrozen) {
229        boolean oldValue = this.isFrozen;
230        this.isFrozen = isFrozen;
231        fireFrozenChanged(oldValue, this.isFrozen);
232    }
233
234    public boolean isFrozen() {
235        return isFrozen;
236    }
237
238    public OsmPrimitivesTableModel getMyTableModel() {
239        return myEntriesTableModel;
240    }
241
242    public OsmPrimitivesTableModel getTheirTableModel() {
243        return theirEntriesTableModel;
244    }
245
246    public OsmPrimitivesTableModel getMergedTableModel() {
247        return mergedEntriesTableModel;
248    }
249
250    public EntriesSelectionModel getMySelectionModel() {
251        return myEntriesSelectionModel;
252    }
253
254    public EntriesSelectionModel getTheirSelectionModel() {
255        return theirEntriesSelectionModel;
256    }
257
258    public EntriesSelectionModel getMergedSelectionModel() {
259        return mergedEntriesSelectionModel;
260    }
261
262    protected void fireModelDataChanged() {
263        myEntriesTableModel.fireTableDataChanged();
264        theirEntriesTableModel.fireTableDataChanged();
265        mergedEntriesTableModel.fireTableDataChanged();
266        setChanged();
267        notifyObservers();
268    }
269
270    protected void copyToTop(ListRole role, int []rows) {
271        copy(role, rows, 0);
272        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
273    }
274
275    /**
276     * Copies the nodes given by indices in rows from the list of my nodes to the
277     * list of merged nodes. Inserts the nodes at the top of the list of merged
278     * nodes.
279     *
280     * @param rows the indices
281     */
282    public void copyMyToTop(int [] rows) {
283        copyToTop(MY_ENTRIES, rows);
284    }
285
286    /**
287     * Copies the nodes given by indices in rows from the list of their nodes to the
288     * list of merged nodes. Inserts the nodes at the top of the list of merged
289     * nodes.
290     *
291     * @param rows the indices
292     */
293    public void copyTheirToTop(int [] rows) {
294        copyToTop(THEIR_ENTRIES, rows);
295    }
296
297    /**
298     * Copies the nodes given by indices in rows from the list of  nodes in source to the
299     * list of merged nodes. Inserts the nodes at the end of the list of merged
300     * nodes.
301     *
302     * @param source the list of nodes to copy from
303     * @param rows the indices
304     */
305
306    public void copyToEnd(ListRole source, int [] rows) {
307        copy(source, rows, getMergedEntriesSize());
308        mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
309
310    }
311
312    /**
313     * Copies the nodes given by indices in rows from the list of my nodes to the
314     * list of merged nodes. Inserts the nodes at the end of the list of merged
315     * nodes.
316     *
317     * @param rows the indices
318     */
319    public void copyMyToEnd(int [] rows) {
320        copyToEnd(MY_ENTRIES, rows);
321    }
322
323    /**
324     * Copies the nodes given by indices in rows from the list of their nodes to the
325     * list of merged nodes. Inserts the nodes at the end of the list of merged
326     * nodes.
327     *
328     * @param rows the indices
329     */
330    public void copyTheirToEnd(int [] rows) {
331        copyToEnd(THEIR_ENTRIES, rows);
332    }
333
334    public void clearMerged() {
335        getMergedEntries().clear();
336        fireModelDataChanged();
337    }
338
339    protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
340        CheckParameterUtil.ensureParameterNotNull(my, "my");
341        CheckParameterUtil.ensureParameterNotNull(their, "their");
342        this.myDataset = my.getDataSet();
343        this.mergedMap = mergedMap;
344        getMergedEntries().clear();
345        getMyEntries().clear();
346        getTheirEntries().clear();
347    }
348
349    protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
350        List<String> items = new ArrayList<String>();
351        for (int i=0; i<Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
352            items.add(deletedIds.get(i).toString());
353        }
354        if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
355            items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
356        }
357        StringBuffer sb = new StringBuffer();
358        sb.append("<html>");
359        sb.append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"));
360        sb.append("<ul>");
361        for (String item: items) {
362            sb.append("<li>").append(item).append("</li>");
363        }
364        sb.append("</ul>");
365        sb.append("</html>");
366        HelpAwareOptionPane.showOptionDialog(
367                Main.parent,
368                sb.toString(),
369                tr("Merging deleted objects failed"),
370                JOptionPane.WARNING_MESSAGE,
371                HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
372        );
373    }
374
375    private void copy(ListRole sourceRole, int[] rows, int position) {
376        if (position < 0 || position > getMergedEntriesSize())
377            throw new IllegalArgumentException();
378        List<T> newItems = new ArrayList<T>(rows.length);
379        List<T> source = entries.get(sourceRole);
380        List<PrimitiveId> deletedIds = new ArrayList<PrimitiveId>();
381        for (int row: rows) {
382            T entry = source.get(row);
383            OsmPrimitive primitive = getMyPrimitive(entry);
384            if (!primitive.isDeleted()) {
385                T clone = cloneEntryForMergedList(entry);
386                newItems.add(clone);
387            } else {
388                deletedIds.add(primitive.getPrimitiveId());
389            }
390        }
391        getMergedEntries().addAll(position, newItems);
392        fireModelDataChanged();
393        if (!deletedIds.isEmpty()) {
394            alertCopyFailedForDeletedPrimitives(deletedIds);
395        }
396    }
397
398    public void copyAll(ListRole source) {
399        getMergedEntries().clear();
400
401        int[] rows = new int[entries.get(source).size()];
402        for (int i=0; i<rows.length; i++) {
403            rows[i] = i;
404        }
405        copy(source, rows, 0);
406    }
407
408    /**
409     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
410     * list of merged nodes. Inserts the nodes before row given by current.
411     *
412     * @param source the list of nodes to copy from
413     * @param rows the indices
414     * @param current the row index before which the nodes are inserted
415     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
416     *
417     */
418    protected void copyBeforeCurrent(ListRole source, int [] rows, int current) {
419        copy(source, rows, current);
420        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
421    }
422
423    /**
424     * Copies the nodes given by indices in rows from the list of my nodes to the
425     * list of merged nodes. Inserts the nodes before row given by current.
426     *
427     * @param rows the indices
428     * @param current the row index before which the nodes are inserted
429     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
430     *
431     */
432    public void copyMyBeforeCurrent(int [] rows, int current) {
433        copyBeforeCurrent(MY_ENTRIES,rows,current);
434    }
435
436    /**
437     * Copies the nodes given by indices in rows from the list of their nodes to the
438     * list of merged nodes. Inserts the nodes before row given by current.
439     *
440     * @param rows the indices
441     * @param current the row index before which the nodes are inserted
442     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
443     *
444     */
445    public void copyTheirBeforeCurrent(int [] rows, int current) {
446        copyBeforeCurrent(THEIR_ENTRIES,rows,current);
447    }
448
449    /**
450     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
451     * list of merged nodes. Inserts the nodes after the row given by current.
452     *
453     * @param source the list of nodes to copy from
454     * @param rows the indices
455     * @param current the row index after which the nodes are inserted
456     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
457     *
458     */
459    protected void copyAfterCurrent(ListRole source, int [] rows, int current) {
460        copy(source, rows, current + 1);
461        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
462        notifyObservers();
463    }
464
465    /**
466     * Copies the nodes given by indices in rows from the list of my nodes to the
467     * list of merged nodes. Inserts the nodes after the row given by current.
468     *
469     * @param rows the indices
470     * @param current the row index after which the nodes are inserted
471     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
472     *
473     */
474    public void copyMyAfterCurrent(int [] rows, int current) {
475        copyAfterCurrent(MY_ENTRIES, rows, current);
476    }
477
478    /**
479     * Copies the nodes given by indices in rows from the list of my nodes to the
480     * list of merged nodes. Inserts the nodes after the row given by current.
481     *
482     * @param rows the indices
483     * @param current the row index after which the nodes are inserted
484     * @exception IllegalArgumentException thrown, if current < 0 or >= #nodes in list of merged nodes
485     *
486     */
487    public void copyTheirAfterCurrent(int [] rows, int current) {
488        copyAfterCurrent(THEIR_ENTRIES, rows, current);
489    }
490
491    /**
492     * Moves the nodes given by indices in rows  up by one position in the list
493     * of merged nodes.
494     *
495     * @param rows the indices
496     *
497     */
498    public void moveUpMerged(int [] rows) {
499        if (rows == null || rows.length == 0)
500            return;
501        if (rows[0] == 0)
502            // can't move up
503            return;
504        List<T> mergedEntries = getMergedEntries();
505        for (int row: rows) {
506            T n = mergedEntries.get(row);
507            mergedEntries.remove(row);
508            mergedEntries.add(row -1, n);
509        }
510        fireModelDataChanged();
511        notifyObservers();
512        mergedEntriesSelectionModel.clearSelection();
513        for (int row: rows) {
514            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
515        }
516    }
517
518    /**
519     * Moves the nodes given by indices in rows down by one position in the list
520     * of merged nodes.
521     *
522     * @param rows the indices
523     */
524    public void moveDownMerged(int [] rows) {
525        if (rows == null || rows.length == 0)
526            return;
527        List<T> mergedEntries = getMergedEntries();
528        if (rows[rows.length -1] == mergedEntries.size() -1)
529            // can't move down
530            return;
531        for (int i = rows.length-1; i>=0;i--) {
532            int row = rows[i];
533            T n = mergedEntries.get(row);
534            mergedEntries.remove(row);
535            mergedEntries.add(row +1, n);
536        }
537        fireModelDataChanged();
538        notifyObservers();
539        mergedEntriesSelectionModel.clearSelection();
540        for (int row: rows) {
541            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
542        }
543    }
544
545    /**
546     * Removes the nodes given by indices in rows from the list
547     * of merged nodes.
548     *
549     * @param rows the indices
550     */
551    public void removeMerged(int [] rows) {
552        if (rows == null || rows.length == 0)
553            return;
554
555        List<T> mergedEntries = getMergedEntries();
556
557        for (int i = rows.length-1; i>=0;i--) {
558            mergedEntries.remove(rows[i]);
559        }
560        fireModelDataChanged();
561        notifyObservers();
562        mergedEntriesSelectionModel.clearSelection();
563    }
564
565    /**
566     * Replies true if the list of my entries and the list of their
567     * entries are equal
568     *
569     * @return true, if the lists are equal; false otherwise
570     */
571    protected boolean myAndTheirEntriesEqual() {
572
573        if (getMyEntriesSize() != getTheirEntriesSize())
574            return false;
575        for (int i=0; i < getMyEntriesSize(); i++) {
576            if (! isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
577                return false;
578        }
579        return true;
580    }
581
582    /**
583     * This an adapter between a {@link JTable} and one of the three entry lists
584     * in the role {@link ListRole} managed by the {@link ListMergeModel}.
585     *
586     * From the point of view of the {@link JTable} it is a {@link TableModel}.
587     *
588     * @see ListMergeModel#getMyTableModel()
589     * @see ListMergeModel#getTheirTableModel()
590     * @see ListMergeModel#getMergedTableModel()
591     */
592    public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel {
593        private final ListRole role;
594
595        /**
596         *
597         * @param role the role
598         */
599        public EntriesTableModel(ListRole role) {
600            this.role = role;
601        }
602
603        @Override
604        public int getRowCount() {
605            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
606            count = Math.max(count, getTheirEntries().size());
607            return count;
608        }
609
610        @Override
611        public Object getValueAt(int row, int column) {
612            if (row < entries.get(role).size())
613                return entries.get(role).get(row);
614            return null;
615        }
616
617        @Override
618        public boolean isCellEditable(int row, int column) {
619            return false;
620        }
621
622        @Override
623        public void setValueAt(Object value, int row, int col) {
624            ListMergeModel.this.setValueAt(this, value,row,col);
625        }
626
627        public ListMergeModel<T> getListMergeModel() {
628            return ListMergeModel.this;
629        }
630
631        /**
632         * replies true if the {@link ListRole} of this {@link EntriesTableModel}
633         * participates in the current {@link ComparePairType}
634         *
635         * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel}
636         * participates in the current {@link ComparePairType}
637         *
638         * @see ComparePairListModel#getSelectedComparePair()
639         */
640        public boolean isParticipatingInCurrentComparePair() {
641            return getComparePairListModel()
642            .getSelectedComparePair()
643            .isParticipatingIn(role);
644        }
645
646        /**
647         * replies true if the entry at <code>row</code> is equal to the entry at the
648         * same position in the opposite list of the current {@link ComparePairType}.
649         *
650         * @param row  the row number
651         * @return true if the entry at <code>row</code> is equal to the entry at the
652         * same position in the opposite list of the current {@link ComparePairType}
653         * @exception IllegalStateException thrown, if this model is not participating in the
654         *   current  {@link ComparePairType}
655         * @see ComparePairType#getOppositeRole(ListRole)
656         * @see #getRole()
657         * @see #getOppositeEntries()
658         */
659        public boolean isSamePositionInOppositeList(int row) {
660            if (!isParticipatingInCurrentComparePair())
661                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
662            if (row >= getEntries().size()) return false;
663            if (row >= getOppositeEntries().size()) return false;
664
665            T e1 = getEntries().get(row);
666            T e2 = getOppositeEntries().get(row);
667            return isEqualEntry(e1, e2);
668        }
669
670        /**
671         * replies true if the entry at the current position is present in the opposite list
672         * of the current {@link ComparePairType}.
673         *
674         * @param row the current row
675         * @return true if the entry at the current position is present in the opposite list
676         * of the current {@link ComparePairType}.
677         * @exception IllegalStateException thrown, if this model is not participating in the
678         *   current  {@link ComparePairType}
679         * @see ComparePairType#getOppositeRole(ListRole)
680         * @see #getRole()
681         * @see #getOppositeEntries()
682         */
683        public boolean isIncludedInOppositeList(int row) {
684            if (!isParticipatingInCurrentComparePair())
685                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
686
687            if (row >= getEntries().size()) return false;
688            T e1 = getEntries().get(row);
689            for (T e2: getOppositeEntries()) {
690                if (isEqualEntry(e1, e2)) return true;
691            }
692            return false;
693        }
694
695        protected List<T> getEntries() {
696            return entries.get(role);
697        }
698
699        /**
700         * replies the opposite list of entries with respect to the current {@link ComparePairType}
701         *
702         * @return the opposite list of entries
703         */
704        protected List<T> getOppositeEntries() {
705            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
706            return entries.get(opposite);
707        }
708
709        public ListRole getRole() {
710            return role;
711        }
712
713        @Override
714        public OsmPrimitive getReferredPrimitive(int idx) {
715            Object value = getValueAt(idx, 1);
716            if (value instanceof OsmPrimitive) {
717                return (OsmPrimitive) value;
718            } else if (value instanceof RelationMember) {
719                return ((RelationMember)value).getMember();
720            } else {
721                Main.error("Unknown object type: "+value);
722                return null;
723            }
724        }
725    }
726
727    /**
728     * This is the selection model to be used in a {@link JTable} which displays
729     * an entry list managed by {@link ListMergeModel}.
730     *
731     * The model ensures that only rows displaying an entry in the entry list
732     * can be selected. "Empty" rows can't be selected.
733     *
734     * @see ListMergeModel#getMySelectionModel()
735     * @see ListMergeModel#getMergedSelectionModel()
736     * @see ListMergeModel#getTheirSelectionModel()
737     *
738     */
739    protected class EntriesSelectionModel extends DefaultListSelectionModel {
740        private final List<T> entries;
741
742        public EntriesSelectionModel(ArrayList<T> nodes) {
743            this.entries = nodes;
744        }
745
746        @Override
747        public void addSelectionInterval(int index0, int index1) {
748            if (entries.isEmpty()) return;
749            if (index0 > entries.size() - 1) return;
750            index0 = Math.min(entries.size()-1, index0);
751            index1 = Math.min(entries.size()-1, index1);
752            super.addSelectionInterval(index0, index1);
753        }
754
755        @Override
756        public void insertIndexInterval(int index, int length, boolean before) {
757            if (entries.isEmpty()) return;
758            if (before) {
759                int newindex = Math.min(entries.size()-1, index);
760                if (newindex < index - length) return;
761                length = length - (index - newindex);
762                super.insertIndexInterval(newindex, length, before);
763            } else {
764                if (index > entries.size() -1) return;
765                length = Math.min(entries.size()-1 - index, length);
766                super.insertIndexInterval(index, length, before);
767            }
768        }
769
770        @Override
771        public void moveLeadSelectionIndex(int leadIndex) {
772            if (entries.isEmpty()) return;
773            leadIndex = Math.max(0, leadIndex);
774            leadIndex = Math.min(entries.size() - 1, leadIndex);
775            super.moveLeadSelectionIndex(leadIndex);
776        }
777
778        @Override
779        public void removeIndexInterval(int index0, int index1) {
780            if (entries.isEmpty()) return;
781            index0 = Math.max(0, index0);
782            index0 = Math.min(entries.size() - 1, index0);
783
784            index1 = Math.max(0, index1);
785            index1 = Math.min(entries.size() - 1, index1);
786            super.removeIndexInterval(index0, index1);
787        }
788
789        @Override
790        public void removeSelectionInterval(int index0, int index1) {
791            if (entries.isEmpty()) return;
792            index0 = Math.max(0, index0);
793            index0 = Math.min(entries.size() - 1, index0);
794
795            index1 = Math.max(0, index1);
796            index1 = Math.min(entries.size() - 1, index1);
797            super.removeSelectionInterval(index0, index1);
798        }
799
800        @Override
801        public void setAnchorSelectionIndex(int anchorIndex) {
802            if (entries.isEmpty()) return;
803            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
804            super.setAnchorSelectionIndex(anchorIndex);
805        }
806
807        @Override
808        public void setLeadSelectionIndex(int leadIndex) {
809            if (entries.isEmpty()) return;
810            leadIndex = Math.min(entries.size() - 1, leadIndex);
811            super.setLeadSelectionIndex(leadIndex);
812        }
813
814        @Override
815        public void setSelectionInterval(int index0, int index1) {
816            if (entries.isEmpty()) return;
817            index0 = Math.max(0, index0);
818            index0 = Math.min(entries.size() - 1, index0);
819
820            index1 = Math.max(0, index1);
821            index1 = Math.min(entries.size() - 1, index1);
822
823            super.setSelectionInterval(index0, index1);
824        }
825    }
826
827    public ComparePairListModel getComparePairListModel() {
828        return this.comparePairListModel;
829    }
830
831    public class ComparePairListModel extends AbstractListModel implements ComboBoxModel {
832
833        private int selectedIdx;
834        private final List<ComparePairType> compareModes;
835
836        public ComparePairListModel() {
837            this.compareModes = new ArrayList<ComparePairType>();
838            compareModes.add(MY_WITH_THEIR);
839            compareModes.add(MY_WITH_MERGED);
840            compareModes.add(THEIR_WITH_MERGED);
841            selectedIdx = 0;
842        }
843
844        @Override
845        public Object getElementAt(int index) {
846            if (index < compareModes.size())
847                return compareModes.get(index);
848            throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
849        }
850
851        @Override
852        public int getSize() {
853            return compareModes.size();
854        }
855
856        @Override
857        public Object getSelectedItem() {
858            return compareModes.get(selectedIdx);
859        }
860
861        @Override
862        public void setSelectedItem(Object anItem) {
863            int i = compareModes.indexOf(anItem);
864            if (i < 0)
865                throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
866            selectedIdx = i;
867            fireModelDataChanged();
868        }
869
870        public ComparePairType getSelectedComparePair() {
871            return compareModes.get(selectedIdx);
872        }
873    }
874}