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