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.command.conflict.ConflictResolveCommand;
032import org.openstreetmap.josm.data.conflict.Conflict;
033import org.openstreetmap.josm.data.osm.DataSet;
034import org.openstreetmap.josm.data.osm.OsmPrimitive;
035import org.openstreetmap.josm.data.osm.PrimitiveId;
036import org.openstreetmap.josm.data.osm.RelationMember;
037import org.openstreetmap.josm.gui.HelpAwareOptionPane;
038import org.openstreetmap.josm.gui.help.HelpUtil;
039import org.openstreetmap.josm.gui.util.ChangeNotifier;
040import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
041import org.openstreetmap.josm.tools.CheckParameterUtil;
042import org.openstreetmap.josm.tools.Utils;
043
044/**
045 * ListMergeModel is a model for interactively comparing and merging two list of entries
046 * of type T. It maintains three lists of entries of type T:
047 * <ol>
048 *   <li>the list of <em>my</em> entries</li>
049 *   <li>the list of <em>their</em> entries</li>
050 *   <li>the list of <em>merged</em> entries</li>
051 * </ol>
052 *
053 * A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s:
054 * <ol>
055 *   <li>the table model and the list selection for for a  {@link JTable} which shows my entries.
056 *    See {@link #getMyTableModel()} and {@link AbstractListMergeModel#getMySelectionModel()}</li>
057 *   <li>dito for their entries and merged entries</li>
058 * </ol>
059 *
060 * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
061 * decisions. {@link PropertyChangeListener}s can register for property value changes of
062 * {@link #FROZEN_PROP}.
063 *
064 * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
065 * <ul>
066 *   <li>{@link AbstractListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li>
067 *   <li>{@link AbstractListMergeModel#isEqualEntry} - checks whether two entries are equals </li>
068 *   <li>{@link AbstractListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
069 *     a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li>
070 * </ul>
071 * A ListMergeModel is used in combination with a {@link AbstractListMerger}.
072 *
073 * @param <T> the type of the list entries
074 * @param <C> the type of conflict resolution command
075 * @see AbstractListMerger
076 */
077public abstract class AbstractListMergeModel<T extends PrimitiveId, C extends ConflictResolveCommand> extends ChangeNotifier {
078    public static final String FROZEN_PROP = AbstractListMergeModel.class.getName() + ".frozen";
079
080    private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5;
081
082    protected Map<ListRole, ArrayList<T>> entries;
083
084    protected EntriesTableModel myEntriesTableModel;
085    protected EntriesTableModel theirEntriesTableModel;
086    protected EntriesTableModel mergedEntriesTableModel;
087
088    protected EntriesSelectionModel myEntriesSelectionModel;
089    protected EntriesSelectionModel theirEntriesSelectionModel;
090    protected EntriesSelectionModel mergedEntriesSelectionModel;
091
092    private final Set<PropertyChangeListener> listeners;
093    private boolean isFrozen;
094    private final ComparePairListModel comparePairListModel;
095
096    private DataSet myDataset;
097    private Map<PrimitiveId, PrimitiveId> mergedMap;
098
099    /**
100     * Creates a clone of an entry of type T suitable to be included in the
101     * list of merged entries
102     *
103     * @param entry the entry
104     * @return the cloned entry
105     */
106    protected abstract T cloneEntryForMergedList(T entry);
107
108    /**
109     * checks whether two entries are equal. This is not necessarily the same as
110     * e1.equals(e2).
111     *
112     * @param e1  the first entry
113     * @param e2  the second entry
114     * @return true, if the entries are equal, false otherwise.
115     */
116    public abstract boolean isEqualEntry(T e1, T e2);
117
118    /**
119     * Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}.
120     *
121     * @param model the table model
122     * @param value  the value to be set
123     * @param row  the row index
124     * @param col the column index
125     *
126     * @see TableModel#setValueAt(Object, int, int)
127     */
128    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
129
130    /**
131     * Replies primitive from my dataset referenced by entry
132     * @param entry entry
133     * @return Primitive from my dataset referenced by entry
134     */
135    public OsmPrimitive getMyPrimitive(T entry) {
136        return getMyPrimitiveById(entry);
137    }
138
139    public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) {
140        OsmPrimitive result = myDataset.getPrimitiveById(entry);
141        if (result == null && mergedMap != null) {
142            PrimitiveId id = mergedMap.get(entry);
143            if (id == null && entry instanceof OsmPrimitive) {
144                id = mergedMap.get(((OsmPrimitive) entry).getPrimitiveId());
145            }
146            if (id != null) {
147                result = myDataset.getPrimitiveById(id);
148            }
149        }
150        return result;
151    }
152
153    protected void buildMyEntriesTableModel() {
154        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
155    }
156
157    protected void buildTheirEntriesTableModel() {
158        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
159    }
160
161    protected void buildMergedEntriesTableModel() {
162        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
163    }
164
165    protected List<T> getMergedEntries() {
166        return entries.get(MERGED_ENTRIES);
167    }
168
169    protected List<T> getMyEntries() {
170        return entries.get(MY_ENTRIES);
171    }
172
173    protected List<T> getTheirEntries() {
174        return entries.get(THEIR_ENTRIES);
175    }
176
177    public int getMyEntriesSize() {
178        return getMyEntries().size();
179    }
180
181    public int getMergedEntriesSize() {
182        return getMergedEntries().size();
183    }
184
185    public int getTheirEntriesSize() {
186        return getTheirEntries().size();
187    }
188
189    /**
190     * Constructs a new {@code ListMergeModel}.
191     */
192    public AbstractListMergeModel() {
193        entries = new EnumMap<>(ListRole.class);
194        for (ListRole role : ListRole.values()) {
195            entries.put(role, new ArrayList<T>());
196        }
197
198        buildMyEntriesTableModel();
199        buildTheirEntriesTableModel();
200        buildMergedEntriesTableModel();
201
202        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
203        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
204        mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
205
206        listeners = new HashSet<>();
207        comparePairListModel = new ComparePairListModel();
208
209        setFrozen(true);
210    }
211
212    public void addPropertyChangeListener(PropertyChangeListener listener) {
213        synchronized (listeners) {
214            if (listener != null && !listeners.contains(listener)) {
215                listeners.add(listener);
216            }
217        }
218    }
219
220    public void removePropertyChangeListener(PropertyChangeListener listener) {
221        synchronized (listeners) {
222            if (listener != null && listeners.contains(listener)) {
223                listeners.remove(listener);
224            }
225        }
226    }
227
228    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
229        synchronized (listeners) {
230            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
231            listeners.forEach(listener -> listener.propertyChange(evt));
232            }
233        }
234
235    public final void setFrozen(boolean isFrozen) {
236        boolean oldValue = this.isFrozen;
237        this.isFrozen = isFrozen;
238        fireFrozenChanged(oldValue, this.isFrozen);
239    }
240
241    public final boolean isFrozen() {
242        return isFrozen;
243    }
244
245    public OsmPrimitivesTableModel getMyTableModel() {
246        return myEntriesTableModel;
247    }
248
249    public OsmPrimitivesTableModel getTheirTableModel() {
250        return theirEntriesTableModel;
251    }
252
253    public OsmPrimitivesTableModel getMergedTableModel() {
254        return mergedEntriesTableModel;
255    }
256
257    public EntriesSelectionModel getMySelectionModel() {
258        return myEntriesSelectionModel;
259    }
260
261    public EntriesSelectionModel getTheirSelectionModel() {
262        return theirEntriesSelectionModel;
263    }
264
265    public EntriesSelectionModel getMergedSelectionModel() {
266        return mergedEntriesSelectionModel;
267    }
268
269    protected void fireModelDataChanged() {
270        myEntriesTableModel.fireTableDataChanged();
271        theirEntriesTableModel.fireTableDataChanged();
272        mergedEntriesTableModel.fireTableDataChanged();
273        fireStateChanged();
274    }
275
276    protected void copyToTop(ListRole role, int ... rows) {
277        copy(role, rows, 0);
278        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
279    }
280
281    /**
282     * Copies the nodes given by indices in rows from the list of my nodes to the
283     * list of merged nodes. Inserts the nodes at the top of the list of merged
284     * nodes.
285     *
286     * @param rows the indices
287     */
288    public void copyMyToTop(int ... rows) {
289        copyToTop(MY_ENTRIES, rows);
290    }
291
292    /**
293     * Copies the nodes given by indices in rows from the list of their nodes to the
294     * list of merged nodes. Inserts the nodes at the top of the list of merged
295     * nodes.
296     *
297     * @param rows the indices
298     */
299    public void copyTheirToTop(int ... rows) {
300        copyToTop(THEIR_ENTRIES, rows);
301    }
302
303    /**
304     * Copies the nodes given by indices in rows from the list of  nodes in source to the
305     * list of merged nodes. Inserts the nodes at the end of the list of merged
306     * nodes.
307     *
308     * @param source the list of nodes to copy from
309     * @param rows the indices
310     */
311
312    public void copyToEnd(ListRole source, int ... rows) {
313        copy(source, rows, getMergedEntriesSize());
314        mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
315
316    }
317
318    /**
319     * Copies the nodes given by indices in rows from the list of my nodes to the
320     * list of merged nodes. Inserts the nodes at the end of the list of merged
321     * nodes.
322     *
323     * @param rows the indices
324     */
325    public void copyMyToEnd(int ... rows) {
326        copyToEnd(MY_ENTRIES, rows);
327    }
328
329    /**
330     * Copies the nodes given by indices in rows from the list of their nodes to the
331     * list of merged nodes. Inserts the nodes at the end of the list of merged
332     * nodes.
333     *
334     * @param rows the indices
335     */
336    public void copyTheirToEnd(int ... rows) {
337        copyToEnd(THEIR_ENTRIES, rows);
338    }
339
340    public void clearMerged() {
341        getMergedEntries().clear();
342        fireModelDataChanged();
343    }
344
345    protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
346        CheckParameterUtil.ensureParameterNotNull(my, "my");
347        CheckParameterUtil.ensureParameterNotNull(their, "their");
348        this.myDataset = my.getDataSet();
349        this.mergedMap = mergedMap;
350        getMergedEntries().clear();
351        getMyEntries().clear();
352        getTheirEntries().clear();
353    }
354
355    protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
356        List<String> items = new ArrayList<>();
357        for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
358            items.add(deletedIds.get(i).toString());
359        }
360        if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
361            items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
362        }
363        StringBuilder sb = new StringBuilder();
364        sb.append("<html>")
365          .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"))
366          .append(Utils.joinAsHtmlUnorderedList(items))
367          .append("</html>");
368        HelpAwareOptionPane.showOptionDialog(
369                Main.parent,
370                sb.toString(),
371                tr("Merging deleted objects failed"),
372                JOptionPane.WARNING_MESSAGE,
373                HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
374        );
375    }
376
377    private void copy(ListRole sourceRole, int[] rows, int position) {
378        if (position < 0 || position > getMergedEntriesSize())
379            throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position);
380        List<T> newItems = new ArrayList<>(rows.length);
381        List<T> source = entries.get(sourceRole);
382        List<PrimitiveId> deletedIds = new ArrayList<>();
383        for (int row: rows) {
384            T entry = source.get(row);
385            OsmPrimitive primitive = getMyPrimitive(entry);
386            if (!primitive.isDeleted()) {
387                T clone = cloneEntryForMergedList(entry);
388                newItems.add(clone);
389            } else {
390                deletedIds.add(primitive.getPrimitiveId());
391            }
392        }
393        getMergedEntries().addAll(position, newItems);
394        fireModelDataChanged();
395        if (!deletedIds.isEmpty()) {
396            alertCopyFailedForDeletedPrimitives(deletedIds);
397        }
398    }
399
400    public void copyAll(ListRole source) {
401        getMergedEntries().clear();
402
403        int[] rows = new int[entries.get(source).size()];
404        for (int i = 0; i < rows.length; i++) {
405            rows[i] = i;
406        }
407        copy(source, rows, 0);
408    }
409
410    /**
411     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
412     * list of merged nodes. Inserts the nodes before row given by current.
413     *
414     * @param source the list of nodes to copy from
415     * @param rows the indices
416     * @param current the row index before which the nodes are inserted
417     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
418     */
419    protected void copyBeforeCurrent(ListRole source, int[] rows, int current) {
420        copy(source, rows, current);
421        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
422    }
423
424    /**
425     * Copies the nodes given by indices in rows from the list of my nodes to the
426     * list of merged nodes. Inserts the nodes before row given by current.
427     *
428     * @param rows the indices
429     * @param current the row index before which the nodes are inserted
430     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
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     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
443     */
444    public void copyTheirBeforeCurrent(int[] rows, int current) {
445        copyBeforeCurrent(THEIR_ENTRIES, rows, current);
446    }
447
448    /**
449     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
450     * list of merged nodes. Inserts the nodes after the row given by current.
451     *
452     * @param source the list of nodes to copy from
453     * @param rows the indices
454     * @param current the row index after which the nodes are inserted
455     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
456     */
457    protected void copyAfterCurrent(ListRole source, int[] rows, int current) {
458        copy(source, rows, current + 1);
459        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
460        fireStateChanged();
461    }
462
463    /**
464     * Copies the nodes given by indices in rows from the list of my nodes to the
465     * list of merged nodes. Inserts the nodes after the row given by current.
466     *
467     * @param rows the indices
468     * @param current the row index after which the nodes are inserted
469     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
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     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
482     */
483    public void copyTheirAfterCurrent(int[] rows, int current) {
484        copyAfterCurrent(THEIR_ENTRIES, rows, current);
485    }
486
487    /**
488     * Moves the nodes given by indices in rows  up by one position in the list
489     * of merged nodes.
490     *
491     * @param rows the indices
492     *
493     */
494    public void moveUpMerged(int ... rows) {
495        if (rows == null || rows.length == 0)
496            return;
497        if (rows[0] == 0)
498            // can't move up
499            return;
500        List<T> mergedEntries = getMergedEntries();
501        for (int row: rows) {
502            T n = mergedEntries.get(row);
503            mergedEntries.remove(row);
504            mergedEntries.add(row -1, n);
505        }
506        fireModelDataChanged();
507        mergedEntriesSelectionModel.setValueIsAdjusting(true);
508        mergedEntriesSelectionModel.clearSelection();
509        for (int row: rows) {
510            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
511        }
512        mergedEntriesSelectionModel.setValueIsAdjusting(false);
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        mergedEntriesSelectionModel.setValueIsAdjusting(true);
536        mergedEntriesSelectionModel.clearSelection();
537        for (int row: rows) {
538            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
539        }
540        mergedEntriesSelectionModel.setValueIsAdjusting(false);
541    }
542
543    /**
544     * Removes the nodes given by indices in rows from the list
545     * of merged nodes.
546     *
547     * @param rows the indices
548     */
549    public void removeMerged(int ... rows) {
550        if (rows == null || rows.length == 0)
551            return;
552
553        List<T> mergedEntries = getMergedEntries();
554
555        for (int i = rows.length-1; i >= 0; i--) {
556            mergedEntries.remove(rows[i]);
557        }
558        fireModelDataChanged();
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 AbstractListMergeModel}.
582     *
583     * From the point of view of the {@link JTable} it is a {@link TableModel}.
584     *
585     * @see AbstractListMergeModel#getMyTableModel()
586     * @see AbstractListMergeModel#getTheirTableModel()
587     * @see AbstractListMergeModel#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            return Math.max(count, getTheirEntries().size());
604        }
605
606        @Override
607        public Object getValueAt(int row, int column) {
608            if (row < entries.get(role).size())
609                return entries.get(role).get(row);
610            return null;
611        }
612
613        @Override
614        public boolean isCellEditable(int row, int column) {
615            return false;
616        }
617
618        @Override
619        public void setValueAt(Object value, int row, int col) {
620            AbstractListMergeModel.this.setValueAt(this, value, row, col);
621        }
622
623        /**
624         * Returns the list merge model.
625         * @return the list merge model
626         */
627        public AbstractListMergeModel<T, C> getListMergeModel() {
628            return AbstractListMergeModel.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 AbstractListMergeModel.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         * @throws IllegalStateException 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         * @throws IllegalStateException 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            return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2));
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 AbstractListMergeModel}.
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 AbstractListMergeModel#getMySelectionModel()
732     * @see AbstractListMergeModel#getMergedSelectionModel()
733     * @see AbstractListMergeModel#getTheirSelectionModel()
734     *
735     */
736    protected class EntriesSelectionModel extends DefaultListSelectionModel {
737        private final transient List<T> entries;
738
739        public EntriesSelectionModel(List<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
875    /**
876     * Builds the command to resolve conflicts in the list.
877     *
878     * @param conflict the conflict data set
879     * @return the command
880     * @throws IllegalStateException if the merge is not yet frozen
881     */
882    public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict);
883}