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