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