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            listeners.forEach(listener -> listener.propertyChange(evt));
229            }
230        }
231
232    public final void setFrozen(boolean isFrozen) {
233        boolean oldValue = this.isFrozen;
234        this.isFrozen = isFrozen;
235        fireFrozenChanged(oldValue, this.isFrozen);
236    }
237
238    public final boolean isFrozen() {
239        return isFrozen;
240    }
241
242    public OsmPrimitivesTableModel getMyTableModel() {
243        return myEntriesTableModel;
244    }
245
246    public OsmPrimitivesTableModel getTheirTableModel() {
247        return theirEntriesTableModel;
248    }
249
250    public OsmPrimitivesTableModel getMergedTableModel() {
251        return mergedEntriesTableModel;
252    }
253
254    public EntriesSelectionModel getMySelectionModel() {
255        return myEntriesSelectionModel;
256    }
257
258    public EntriesSelectionModel getTheirSelectionModel() {
259        return theirEntriesSelectionModel;
260    }
261
262    public EntriesSelectionModel getMergedSelectionModel() {
263        return mergedEntriesSelectionModel;
264    }
265
266    protected void fireModelDataChanged() {
267        myEntriesTableModel.fireTableDataChanged();
268        theirEntriesTableModel.fireTableDataChanged();
269        mergedEntriesTableModel.fireTableDataChanged();
270        fireStateChanged();
271    }
272
273    protected void copyToTop(ListRole role, int ... rows) {
274        copy(role, rows, 0);
275        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
276    }
277
278    /**
279     * Copies the nodes given by indices in rows from the list of my nodes to the
280     * list of merged nodes. Inserts the nodes at the top of the list of merged
281     * nodes.
282     *
283     * @param rows the indices
284     */
285    public void copyMyToTop(int ... rows) {
286        copyToTop(MY_ENTRIES, rows);
287    }
288
289    /**
290     * Copies the nodes given by indices in rows from the list of their nodes to the
291     * list of merged nodes. Inserts the nodes at the top of the list of merged
292     * nodes.
293     *
294     * @param rows the indices
295     */
296    public void copyTheirToTop(int ... rows) {
297        copyToTop(THEIR_ENTRIES, rows);
298    }
299
300    /**
301     * Copies the nodes given by indices in rows from the list of  nodes in source to the
302     * list of merged nodes. Inserts the nodes at the end of the list of merged
303     * nodes.
304     *
305     * @param source the list of nodes to copy from
306     * @param rows the indices
307     */
308
309    public void copyToEnd(ListRole source, int ... rows) {
310        copy(source, rows, getMergedEntriesSize());
311        mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
312
313    }
314
315    /**
316     * Copies the nodes given by indices in rows from the list of my nodes to the
317     * list of merged nodes. Inserts the nodes at the end of the list of merged
318     * nodes.
319     *
320     * @param rows the indices
321     */
322    public void copyMyToEnd(int ... rows) {
323        copyToEnd(MY_ENTRIES, rows);
324    }
325
326    /**
327     * Copies the nodes given by indices in rows from the list of their nodes to the
328     * list of merged nodes. Inserts the nodes at the end of the list of merged
329     * nodes.
330     *
331     * @param rows the indices
332     */
333    public void copyTheirToEnd(int ... rows) {
334        copyToEnd(THEIR_ENTRIES, rows);
335    }
336
337    public void clearMerged() {
338        getMergedEntries().clear();
339        fireModelDataChanged();
340    }
341
342    protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
343        CheckParameterUtil.ensureParameterNotNull(my, "my");
344        CheckParameterUtil.ensureParameterNotNull(their, "their");
345        this.myDataset = my.getDataSet();
346        this.mergedMap = mergedMap;
347        getMergedEntries().clear();
348        getMyEntries().clear();
349        getTheirEntries().clear();
350    }
351
352    protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
353        List<String> items = new ArrayList<>();
354        for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
355            items.add(deletedIds.get(i).toString());
356        }
357        if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
358            items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
359        }
360        StringBuilder sb = new StringBuilder();
361        sb.append("<html>")
362          .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"))
363          .append(Utils.joinAsHtmlUnorderedList(items))
364          .append("</html>");
365        HelpAwareOptionPane.showOptionDialog(
366                Main.parent,
367                sb.toString(),
368                tr("Merging deleted objects failed"),
369                JOptionPane.WARNING_MESSAGE,
370                HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
371        );
372    }
373
374    private void copy(ListRole sourceRole, int[] rows, int position) {
375        if (position < 0 || position > getMergedEntriesSize())
376            throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position);
377        List<T> newItems = new ArrayList<>(rows.length);
378        List<T> source = entries.get(sourceRole);
379        List<PrimitiveId> deletedIds = new ArrayList<>();
380        for (int row: rows) {
381            T entry = source.get(row);
382            OsmPrimitive primitive = getMyPrimitive(entry);
383            if (!primitive.isDeleted()) {
384                T clone = cloneEntryForMergedList(entry);
385                newItems.add(clone);
386            } else {
387                deletedIds.add(primitive.getPrimitiveId());
388            }
389        }
390        getMergedEntries().addAll(position, newItems);
391        fireModelDataChanged();
392        if (!deletedIds.isEmpty()) {
393            alertCopyFailedForDeletedPrimitives(deletedIds);
394        }
395    }
396
397    public void copyAll(ListRole source) {
398        getMergedEntries().clear();
399
400        int[] rows = new int[entries.get(source).size()];
401        for (int i = 0; i < rows.length; i++) {
402            rows[i] = i;
403        }
404        copy(source, rows, 0);
405    }
406
407    /**
408     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
409     * list of merged nodes. Inserts the nodes before row given by current.
410     *
411     * @param source the list of nodes to copy from
412     * @param rows the indices
413     * @param current the row index before which the nodes are inserted
414     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
415     */
416    protected void copyBeforeCurrent(ListRole source, int[] rows, int current) {
417        copy(source, rows, current);
418        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
419    }
420
421    /**
422     * Copies the nodes given by indices in rows from the list of my nodes to the
423     * list of merged nodes. Inserts the nodes before row given by current.
424     *
425     * @param rows the indices
426     * @param current the row index before which the nodes are inserted
427     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
428     */
429    public void copyMyBeforeCurrent(int[] rows, int current) {
430        copyBeforeCurrent(MY_ENTRIES, rows, current);
431    }
432
433    /**
434     * Copies the nodes given by indices in rows from the list of their nodes to the
435     * list of merged nodes. Inserts the nodes before row given by current.
436     *
437     * @param rows the indices
438     * @param current the row index before which the nodes are inserted
439     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
440     */
441    public void copyTheirBeforeCurrent(int[] rows, int current) {
442        copyBeforeCurrent(THEIR_ENTRIES, rows, current);
443    }
444
445    /**
446     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
447     * list of merged nodes. Inserts the nodes after the row given by current.
448     *
449     * @param source the list of nodes to copy from
450     * @param rows the indices
451     * @param current the row index after which the nodes are inserted
452     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
453     */
454    protected void copyAfterCurrent(ListRole source, int[] rows, int current) {
455        copy(source, rows, current + 1);
456        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
457        fireStateChanged();
458    }
459
460    /**
461     * Copies the nodes given by indices in rows from the list of my nodes to the
462     * list of merged nodes. Inserts the nodes after the row given by current.
463     *
464     * @param rows the indices
465     * @param current the row index after which the nodes are inserted
466     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
467     */
468    public void copyMyAfterCurrent(int[] rows, int current) {
469        copyAfterCurrent(MY_ENTRIES, rows, current);
470    }
471
472    /**
473     * Copies the nodes given by indices in rows from the list of my nodes to the
474     * list of merged nodes. Inserts the nodes after the row given by current.
475     *
476     * @param rows the indices
477     * @param current the row index after which the nodes are inserted
478     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
479     */
480    public void copyTheirAfterCurrent(int[] rows, int current) {
481        copyAfterCurrent(THEIR_ENTRIES, rows, current);
482    }
483
484    /**
485     * Moves the nodes given by indices in rows  up by one position in the list
486     * of merged nodes.
487     *
488     * @param rows the indices
489     *
490     */
491    public void moveUpMerged(int ... rows) {
492        if (rows == null || rows.length == 0)
493            return;
494        if (rows[0] == 0)
495            // can't move up
496            return;
497        List<T> mergedEntries = getMergedEntries();
498        for (int row: rows) {
499            T n = mergedEntries.get(row);
500            mergedEntries.remove(row);
501            mergedEntries.add(row -1, n);
502        }
503        fireModelDataChanged();
504        mergedEntriesSelectionModel.clearSelection();
505        for (int row: rows) {
506            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
507        }
508    }
509
510    /**
511     * Moves the nodes given by indices in rows down by one position in the list
512     * of merged nodes.
513     *
514     * @param rows the indices
515     */
516    public void moveDownMerged(int ... rows) {
517        if (rows == null || rows.length == 0)
518            return;
519        List<T> mergedEntries = getMergedEntries();
520        if (rows[rows.length -1] == mergedEntries.size() -1)
521            // can't move down
522            return;
523        for (int i = rows.length-1; i >= 0; i--) {
524            int row = rows[i];
525            T n = mergedEntries.get(row);
526            mergedEntries.remove(row);
527            mergedEntries.add(row +1, n);
528        }
529        fireModelDataChanged();
530        mergedEntriesSelectionModel.clearSelection();
531        for (int row: rows) {
532            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
533        }
534    }
535
536    /**
537     * Removes the nodes given by indices in rows from the list
538     * of merged nodes.
539     *
540     * @param rows the indices
541     */
542    public void removeMerged(int ... rows) {
543        if (rows == null || rows.length == 0)
544            return;
545
546        List<T> mergedEntries = getMergedEntries();
547
548        for (int i = rows.length-1; i >= 0; i--) {
549            mergedEntries.remove(rows[i]);
550        }
551        fireModelDataChanged();
552        mergedEntriesSelectionModel.clearSelection();
553    }
554
555    /**
556     * Replies true if the list of my entries and the list of their
557     * entries are equal
558     *
559     * @return true, if the lists are equal; false otherwise
560     */
561    protected boolean myAndTheirEntriesEqual() {
562
563        if (getMyEntriesSize() != getTheirEntriesSize())
564            return false;
565        for (int i = 0; i < getMyEntriesSize(); i++) {
566            if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
567                return false;
568        }
569        return true;
570    }
571
572    /**
573     * This an adapter between a {@link JTable} and one of the three entry lists
574     * in the role {@link ListRole} managed by the {@link ListMergeModel}.
575     *
576     * From the point of view of the {@link JTable} it is a {@link TableModel}.
577     *
578     * @see ListMergeModel#getMyTableModel()
579     * @see ListMergeModel#getTheirTableModel()
580     * @see ListMergeModel#getMergedTableModel()
581     */
582    public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel {
583        private final ListRole role;
584
585        /**
586         *
587         * @param role the role
588         */
589        public EntriesTableModel(ListRole role) {
590            this.role = role;
591        }
592
593        @Override
594        public int getRowCount() {
595            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
596            return Math.max(count, getTheirEntries().size());
597        }
598
599        @Override
600        public Object getValueAt(int row, int column) {
601            if (row < entries.get(role).size())
602                return entries.get(role).get(row);
603            return null;
604        }
605
606        @Override
607        public boolean isCellEditable(int row, int column) {
608            return false;
609        }
610
611        @Override
612        public void setValueAt(Object value, int row, int col) {
613            ListMergeModel.this.setValueAt(this, value, row, col);
614        }
615
616        public ListMergeModel<T> getListMergeModel() {
617            return ListMergeModel.this;
618        }
619
620        /**
621         * replies true if the {@link ListRole} of this {@link EntriesTableModel}
622         * participates in the current {@link ComparePairType}
623         *
624         * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel}
625         * participates in the current {@link ComparePairType}
626         *
627         * @see ListMergeModel.ComparePairListModel#getSelectedComparePair()
628         */
629        public boolean isParticipatingInCurrentComparePair() {
630            return getComparePairListModel()
631            .getSelectedComparePair()
632            .isParticipatingIn(role);
633        }
634
635        /**
636         * replies true if the entry at <code>row</code> is equal to the entry at the
637         * same position in the opposite list of the current {@link ComparePairType}.
638         *
639         * @param row  the row number
640         * @return true if the entry at <code>row</code> is equal to the entry at the
641         * same position in the opposite list of the current {@link ComparePairType}
642         * @throws IllegalStateException if this model is not participating in the
643         *   current  {@link ComparePairType}
644         * @see ComparePairType#getOppositeRole(ListRole)
645         * @see #getRole()
646         * @see #getOppositeEntries()
647         */
648        public boolean isSamePositionInOppositeList(int row) {
649            if (!isParticipatingInCurrentComparePair())
650                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
651            if (row >= getEntries().size()) return false;
652            if (row >= getOppositeEntries().size()) return false;
653
654            T e1 = getEntries().get(row);
655            T e2 = getOppositeEntries().get(row);
656            return isEqualEntry(e1, e2);
657        }
658
659        /**
660         * replies true if the entry at the current position is present in the opposite list
661         * of the current {@link ComparePairType}.
662         *
663         * @param row the current row
664         * @return true if the entry at the current position is present in the opposite list
665         * of the current {@link ComparePairType}.
666         * @throws IllegalStateException if this model is not participating in the
667         *   current {@link ComparePairType}
668         * @see ComparePairType#getOppositeRole(ListRole)
669         * @see #getRole()
670         * @see #getOppositeEntries()
671         */
672        public boolean isIncludedInOppositeList(int row) {
673            if (!isParticipatingInCurrentComparePair())
674                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
675
676            if (row >= getEntries().size()) return false;
677            T e1 = getEntries().get(row);
678            return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2));
679            }
680
681        protected List<T> getEntries() {
682            return entries.get(role);
683        }
684
685        /**
686         * replies the opposite list of entries with respect to the current {@link ComparePairType}
687         *
688         * @return the opposite list of entries
689         */
690        protected List<T> getOppositeEntries() {
691            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
692            return entries.get(opposite);
693        }
694
695        public ListRole getRole() {
696            return role;
697        }
698
699        @Override
700        public OsmPrimitive getReferredPrimitive(int idx) {
701            Object value = getValueAt(idx, 1);
702            if (value instanceof OsmPrimitive) {
703                return (OsmPrimitive) value;
704            } else if (value instanceof RelationMember) {
705                return ((RelationMember) value).getMember();
706            } else {
707                Main.error("Unknown object type: "+value);
708                return null;
709            }
710        }
711    }
712
713    /**
714     * This is the selection model to be used in a {@link JTable} which displays
715     * an entry list managed by {@link ListMergeModel}.
716     *
717     * The model ensures that only rows displaying an entry in the entry list
718     * can be selected. "Empty" rows can't be selected.
719     *
720     * @see ListMergeModel#getMySelectionModel()
721     * @see ListMergeModel#getMergedSelectionModel()
722     * @see ListMergeModel#getTheirSelectionModel()
723     *
724     */
725    protected class EntriesSelectionModel extends DefaultListSelectionModel {
726        private final transient List<T> entries;
727
728        public EntriesSelectionModel(List<T> nodes) {
729            this.entries = nodes;
730        }
731
732        @Override
733        public void addSelectionInterval(int index0, int index1) {
734            if (entries.isEmpty()) return;
735            if (index0 > entries.size() - 1) return;
736            index0 = Math.min(entries.size()-1, index0);
737            index1 = Math.min(entries.size()-1, index1);
738            super.addSelectionInterval(index0, index1);
739        }
740
741        @Override
742        public void insertIndexInterval(int index, int length, boolean before) {
743            if (entries.isEmpty()) return;
744            if (before) {
745                int newindex = Math.min(entries.size()-1, index);
746                if (newindex < index - length) return;
747                length = length - (index - newindex);
748                super.insertIndexInterval(newindex, length, before);
749            } else {
750                if (index > entries.size() -1) return;
751                length = Math.min(entries.size()-1 - index, length);
752                super.insertIndexInterval(index, length, before);
753            }
754        }
755
756        @Override
757        public void moveLeadSelectionIndex(int leadIndex) {
758            if (entries.isEmpty()) return;
759            leadIndex = Math.max(0, leadIndex);
760            leadIndex = Math.min(entries.size() - 1, leadIndex);
761            super.moveLeadSelectionIndex(leadIndex);
762        }
763
764        @Override
765        public void removeIndexInterval(int index0, int index1) {
766            if (entries.isEmpty()) return;
767            index0 = Math.max(0, index0);
768            index0 = Math.min(entries.size() - 1, index0);
769
770            index1 = Math.max(0, index1);
771            index1 = Math.min(entries.size() - 1, index1);
772            super.removeIndexInterval(index0, index1);
773        }
774
775        @Override
776        public void removeSelectionInterval(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.removeSelectionInterval(index0, index1);
784        }
785
786        @Override
787        public void setAnchorSelectionIndex(int anchorIndex) {
788            if (entries.isEmpty()) return;
789            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
790            super.setAnchorSelectionIndex(anchorIndex);
791        }
792
793        @Override
794        public void setLeadSelectionIndex(int leadIndex) {
795            if (entries.isEmpty()) return;
796            leadIndex = Math.min(entries.size() - 1, leadIndex);
797            super.setLeadSelectionIndex(leadIndex);
798        }
799
800        @Override
801        public void setSelectionInterval(int index0, int index1) {
802            if (entries.isEmpty()) return;
803            index0 = Math.max(0, index0);
804            index0 = Math.min(entries.size() - 1, index0);
805
806            index1 = Math.max(0, index1);
807            index1 = Math.min(entries.size() - 1, index1);
808
809            super.setSelectionInterval(index0, index1);
810        }
811    }
812
813    public ComparePairListModel getComparePairListModel() {
814        return this.comparePairListModel;
815    }
816
817    public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
818
819        private int selectedIdx;
820        private final List<ComparePairType> compareModes;
821
822        /**
823         * Constructs a new {@code ComparePairListModel}.
824         */
825        public ComparePairListModel() {
826            this.compareModes = new ArrayList<>();
827            compareModes.add(MY_WITH_THEIR);
828            compareModes.add(MY_WITH_MERGED);
829            compareModes.add(THEIR_WITH_MERGED);
830            selectedIdx = 0;
831        }
832
833        @Override
834        public ComparePairType getElementAt(int index) {
835            if (index < compareModes.size())
836                return compareModes.get(index);
837            throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
838        }
839
840        @Override
841        public int getSize() {
842            return compareModes.size();
843        }
844
845        @Override
846        public Object getSelectedItem() {
847            return compareModes.get(selectedIdx);
848        }
849
850        @Override
851        public void setSelectedItem(Object anItem) {
852            int i = compareModes.indexOf(anItem);
853            if (i < 0)
854                throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
855            selectedIdx = i;
856            fireModelDataChanged();
857        }
858
859        public ComparePairType getSelectedComparePair() {
860            return compareModes.get(selectedIdx);
861        }
862    }
863}