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