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