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