001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.relation;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.EnumSet;
009import java.util.HashSet;
010import java.util.Iterator;
011import java.util.List;
012import java.util.Set;
013import java.util.TreeSet;
014import java.util.concurrent.CopyOnWriteArrayList;
015
016import javax.swing.DefaultListSelectionModel;
017import javax.swing.ListSelectionModel;
018import javax.swing.event.TableModelEvent;
019import javax.swing.event.TableModelListener;
020import javax.swing.table.AbstractTableModel;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.data.SelectionChangedListener;
024import org.openstreetmap.josm.data.osm.DataSet;
025import org.openstreetmap.josm.data.osm.OsmPrimitive;
026import org.openstreetmap.josm.data.osm.Relation;
027import org.openstreetmap.josm.data.osm.RelationMember;
028import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
029import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
030import org.openstreetmap.josm.data.osm.event.DataSetListener;
031import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
032import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
033import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
034import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
035import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
036import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
037import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
038import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
039import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
040import org.openstreetmap.josm.gui.layer.OsmDataLayer;
041import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
042import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
043import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
044import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
045import org.openstreetmap.josm.gui.util.GuiHelper;
046import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
047
048public class MemberTableModel extends AbstractTableModel
049implements TableModelListener, SelectionChangedListener, DataSetListener, OsmPrimitivesTableModel {
050
051    /**
052     * data of the table model: The list of members and the cached WayConnectionType of each member.
053     **/
054    private final transient List<RelationMember> members;
055    private transient List<WayConnectionType> connectionType;
056
057    private DefaultListSelectionModel listSelectionModel;
058    private final CopyOnWriteArrayList<IMemberModelListener> listeners;
059    private final transient OsmDataLayer layer;
060    private final transient TaggingPresetHandler presetHandler;
061
062    private final transient WayConnectionTypeCalculator wayConnectionTypeCalculator = new WayConnectionTypeCalculator();
063    private final transient RelationSorter relationSorter = new RelationSorter();
064
065    /**
066     * constructor
067     * @param layer data layer
068     * @param presetHandler tagging preset handler
069     */
070    public MemberTableModel(OsmDataLayer layer, TaggingPresetHandler presetHandler) {
071        members = new ArrayList<>();
072        listeners = new CopyOnWriteArrayList<>();
073        this.layer = layer;
074        this.presetHandler = presetHandler;
075        addTableModelListener(this);
076    }
077
078    /**
079     * Returns the data layer.
080     * @return the data layer
081     */
082    public OsmDataLayer getLayer() {
083        return layer;
084    }
085
086    public void register() {
087        DataSet.addSelectionListener(this);
088        getLayer().data.addDataSetListener(this);
089    }
090
091    public void unregister() {
092        DataSet.removeSelectionListener(this);
093        getLayer().data.removeDataSetListener(this);
094    }
095
096    /* --------------------------------------------------------------------------- */
097    /* Interface SelectionChangedListener                                          */
098    /* --------------------------------------------------------------------------- */
099    @Override
100    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
101        if (Main.main.getEditLayer() != this.layer) return;
102        // just trigger a repaint
103        Collection<RelationMember> sel = getSelectedMembers();
104        fireTableDataChanged();
105        setSelectedMembers(sel);
106    }
107
108    /* --------------------------------------------------------------------------- */
109    /* Interface DataSetListener                                                   */
110    /* --------------------------------------------------------------------------- */
111    @Override
112    public void dataChanged(DataChangedEvent event) {
113        // just trigger a repaint - the display name of the relation members may have changed
114        Collection<RelationMember> sel = getSelectedMembers();
115        GuiHelper.runInEDT(new Runnable() {
116            public void run() {
117                fireTableDataChanged();
118            }
119        });
120        setSelectedMembers(sel);
121    }
122
123    @Override
124    public void nodeMoved(NodeMovedEvent event) {
125        // ignore
126    }
127
128    @Override
129    public void primitivesAdded(PrimitivesAddedEvent event) {
130        // ignore
131    }
132
133    @Override
134    public void primitivesRemoved(PrimitivesRemovedEvent event) {
135        // ignore - the relation in the editor might become out of sync with the relation
136        // in the dataset. We will deal with it when the relation editor is closed or
137        // when the changes in the editor are applied.
138    }
139
140    @Override
141    public void relationMembersChanged(RelationMembersChangedEvent event) {
142        // ignore - the relation in the editor might become out of sync with the relation
143        // in the dataset. We will deal with it when the relation editor is closed or
144        // when the changes in the editor are applied.
145    }
146
147    @Override
148    public void tagsChanged(TagsChangedEvent event) {
149        // just refresh the respective table cells
150        //
151        Collection<RelationMember> sel = getSelectedMembers();
152        for (int i = 0; i < members.size(); i++) {
153            if (members.get(i).getMember() == event.getPrimitive()) {
154                fireTableCellUpdated(i, 1 /* the column with the primitive name */);
155            }
156        }
157        setSelectedMembers(sel);
158    }
159
160    @Override
161    public void wayNodesChanged(WayNodesChangedEvent event) {
162        // ignore
163    }
164
165    @Override
166    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
167        // ignore
168    }
169
170    /* --------------------------------------------------------------------------- */
171
172    public void addMemberModelListener(IMemberModelListener listener) {
173        if (listener != null) {
174            listeners.addIfAbsent(listener);
175        }
176    }
177
178    public void removeMemberModelListener(IMemberModelListener listener) {
179        listeners.remove(listener);
180    }
181
182    protected void fireMakeMemberVisible(int index) {
183        for (IMemberModelListener listener : listeners) {
184            listener.makeMemberVisible(index);
185        }
186    }
187
188    public void populate(Relation relation) {
189        members.clear();
190        if (relation != null) {
191            // make sure we work with clones of the relation members in the model.
192            members.addAll(new Relation(relation).getMembers());
193        }
194        fireTableDataChanged();
195    }
196
197    @Override
198    public int getColumnCount() {
199        return 3;
200    }
201
202    @Override
203    public int getRowCount() {
204        return members.size();
205    }
206
207    @Override
208    public Object getValueAt(int rowIndex, int columnIndex) {
209        switch (columnIndex) {
210        case 0:
211            return members.get(rowIndex).getRole();
212        case 1:
213            return members.get(rowIndex).getMember();
214        case 2:
215            return getWayConnection(rowIndex);
216        }
217        // should not happen
218        return null;
219    }
220
221    @Override
222    public boolean isCellEditable(int rowIndex, int columnIndex) {
223        return columnIndex == 0;
224    }
225
226    @Override
227    public void setValueAt(Object value, int rowIndex, int columnIndex) {
228        // fix #10524 - IndexOutOfBoundsException: Index: 2, Size: 2
229        if (rowIndex >= members.size()) {
230            return;
231        }
232        RelationMember member = members.get(rowIndex);
233        RelationMember newMember = new RelationMember(value.toString(), member.getMember());
234        members.remove(rowIndex);
235        members.add(rowIndex, newMember);
236    }
237
238    @Override
239    public OsmPrimitive getReferredPrimitive(int idx) {
240        return members.get(idx).getMember();
241    }
242
243    public void moveUp(int[] selectedRows) {
244        if (!canMoveUp(selectedRows))
245            return;
246
247        for (int row : selectedRows) {
248            RelationMember member1 = members.get(row);
249            RelationMember member2 = members.get(row - 1);
250            members.set(row, member2);
251            members.set(row - 1, member1);
252        }
253        fireTableDataChanged();
254        getSelectionModel().setValueIsAdjusting(true);
255        getSelectionModel().clearSelection();
256        for (int row : selectedRows) {
257            row--;
258            getSelectionModel().addSelectionInterval(row, row);
259        }
260        getSelectionModel().setValueIsAdjusting(false);
261        fireMakeMemberVisible(selectedRows[0] - 1);
262    }
263
264    public void moveDown(int[] selectedRows) {
265        if (!canMoveDown(selectedRows))
266            return;
267
268        for (int i = selectedRows.length - 1; i >= 0; i--) {
269            int row = selectedRows[i];
270            RelationMember member1 = members.get(row);
271            RelationMember member2 = members.get(row + 1);
272            members.set(row, member2);
273            members.set(row + 1, member1);
274        }
275        fireTableDataChanged();
276        getSelectionModel();
277        getSelectionModel().setValueIsAdjusting(true);
278        getSelectionModel().clearSelection();
279        for (int row : selectedRows) {
280            row++;
281            getSelectionModel().addSelectionInterval(row, row);
282        }
283        getSelectionModel().setValueIsAdjusting(false);
284        fireMakeMemberVisible(selectedRows[0] + 1);
285    }
286
287    public void remove(int[] selectedRows) {
288        if (!canRemove(selectedRows))
289            return;
290        int offset = 0;
291        for (int row : selectedRows) {
292            row -= offset;
293            if (members.size() > row) {
294                members.remove(row);
295                offset++;
296            }
297        }
298        fireTableDataChanged();
299    }
300
301    public boolean canMoveUp(int[] rows) {
302        if (rows == null || rows.length == 0)
303            return false;
304        Arrays.sort(rows);
305        return rows[0] > 0 && !members.isEmpty();
306    }
307
308    public boolean canMoveDown(int[] rows) {
309        if (rows == null || rows.length == 0)
310            return false;
311        Arrays.sort(rows);
312        return !members.isEmpty() && rows[rows.length - 1] < members.size() - 1;
313    }
314
315    public boolean canRemove(int[] rows) {
316        if (rows == null || rows.length == 0)
317            return false;
318        return true;
319    }
320
321    public DefaultListSelectionModel getSelectionModel() {
322        if (listSelectionModel == null) {
323            listSelectionModel = new DefaultListSelectionModel();
324            listSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
325        }
326        return listSelectionModel;
327    }
328
329    public void removeMembersReferringTo(List<? extends OsmPrimitive> primitives) {
330        if (primitives == null)
331            return;
332        Iterator<RelationMember> it = members.iterator();
333        while (it.hasNext()) {
334            RelationMember member = it.next();
335            if (primitives.contains(member.getMember())) {
336                it.remove();
337            }
338        }
339        fireTableDataChanged();
340    }
341
342    public void applyToRelation(Relation relation) {
343        relation.setMembers(members);
344    }
345
346    public boolean hasSameMembersAs(Relation relation) {
347        if (relation == null)
348            return false;
349        if (relation.getMembersCount() != members.size())
350            return false;
351        for (int i = 0; i < relation.getMembersCount(); i++) {
352            if (!relation.getMember(i).equals(members.get(i)))
353                return false;
354        }
355        return true;
356    }
357
358    /**
359     * Replies the set of incomplete primitives
360     *
361     * @return the set of incomplete primitives
362     */
363    public Set<OsmPrimitive> getIncompleteMemberPrimitives() {
364        Set<OsmPrimitive> ret = new HashSet<>();
365        for (RelationMember member : members) {
366            if (member.getMember().isIncomplete()) {
367                ret.add(member.getMember());
368            }
369        }
370        return ret;
371    }
372
373    /**
374     * Replies the set of selected incomplete primitives
375     *
376     * @return the set of selected incomplete primitives
377     */
378    public Set<OsmPrimitive> getSelectedIncompleteMemberPrimitives() {
379        Set<OsmPrimitive> ret = new HashSet<>();
380        for (RelationMember member : getSelectedMembers()) {
381            if (member.getMember().isIncomplete()) {
382                ret.add(member.getMember());
383            }
384        }
385        return ret;
386    }
387
388    /**
389     * Replies true if at least one the relation members is incomplete
390     *
391     * @return true if at least one the relation members is incomplete
392     */
393    public boolean hasIncompleteMembers() {
394        for (RelationMember member : members) {
395            if (member.getMember().isIncomplete())
396                return true;
397        }
398        return false;
399    }
400
401    /**
402     * Replies true if at least one of the selected members is incomplete
403     *
404     * @return true if at least one of the selected members is incomplete
405     */
406    public boolean hasIncompleteSelectedMembers() {
407        for (RelationMember member : getSelectedMembers()) {
408            if (member.getMember().isIncomplete())
409                return true;
410        }
411        return false;
412    }
413
414    protected List<Integer> getSelectedIndices() {
415        List<Integer> selectedIndices = new ArrayList<>();
416        for (int i = 0; i < members.size(); i++) {
417            if (getSelectionModel().isSelectedIndex(i)) {
418                selectedIndices.add(i);
419            }
420        }
421        return selectedIndices;
422    }
423
424    private void addMembersAtIndex(List<? extends OsmPrimitive> primitives, int index) {
425        final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(EnumSet.of(TaggingPresetType.RELATION),
426                presetHandler.getSelection().iterator().next().getKeys(), false);
427        if (primitives == null)
428            return;
429        int idx = index;
430        for (OsmPrimitive primitive : primitives) {
431            Set<String> potentialRoles = new TreeSet<>();
432            for (TaggingPreset tp : presets) {
433                String suggestedRole = tp.suggestRoleForOsmPrimitive(primitive);
434                if (suggestedRole != null) {
435                    potentialRoles.add(suggestedRole);
436                }
437            }
438            // TODO: propose user to choose role among potential ones instead of picking first one
439            final String role = potentialRoles.isEmpty() ? null : potentialRoles.iterator().next();
440            RelationMember member = new RelationMember(role == null ? "" : role, primitive);
441            members.add(idx++, member);
442        }
443        fireTableDataChanged();
444        getSelectionModel().clearSelection();
445        getSelectionModel().addSelectionInterval(index, index + primitives.size() - 1);
446        fireMakeMemberVisible(index);
447    }
448
449    public void addMembersAtBeginning(List<? extends OsmPrimitive> primitives) {
450        addMembersAtIndex(primitives, 0);
451    }
452
453    public void addMembersAtEnd(List<? extends OsmPrimitive> primitives) {
454        addMembersAtIndex(primitives, members.size());
455    }
456
457    public void addMembersBeforeIdx(List<? extends OsmPrimitive> primitives, int idx) {
458        addMembersAtIndex(primitives, idx);
459    }
460
461    public void addMembersAfterIdx(List<? extends OsmPrimitive> primitives, int idx) {
462        addMembersAtIndex(primitives, idx + 1);
463    }
464
465    /**
466     * Replies the number of members which refer to a particular primitive
467     *
468     * @param primitive the primitive
469     * @return the number of members which refer to a particular primitive
470     */
471    public int getNumMembersWithPrimitive(OsmPrimitive primitive) {
472        int count = 0;
473        for (RelationMember member : members) {
474            if (member.getMember().equals(primitive)) {
475                count++;
476            }
477        }
478        return count;
479    }
480
481    /**
482     * updates the role of the members given by the indices in <code>idx</code>
483     *
484     * @param idx the array of indices
485     * @param role the new role
486     */
487    public void updateRole(int[] idx, String role) {
488        if (idx == null || idx.length == 0)
489            return;
490        for (int row : idx) {
491            // fix #7885 - IndexOutOfBoundsException: Index: 39, Size: 39
492            if (row >= members.size()) {
493                continue;
494            }
495            RelationMember oldMember = members.get(row);
496            RelationMember newMember = new RelationMember(role, oldMember.getMember());
497            members.remove(row);
498            members.add(row, newMember);
499        }
500        fireTableDataChanged();
501        for (int row : idx) {
502            getSelectionModel().addSelectionInterval(row, row);
503        }
504    }
505
506    /**
507     * Get the currently selected relation members
508     *
509     * @return a collection with the currently selected relation members
510     */
511    public Collection<RelationMember> getSelectedMembers() {
512        List<RelationMember> selectedMembers = new ArrayList<>();
513        for (int i : getSelectedIndices()) {
514            selectedMembers.add(members.get(i));
515        }
516        return selectedMembers;
517    }
518
519    /**
520     * Replies the set of selected referers. Never null, but may be empty.
521     *
522     * @return the set of selected referers
523     */
524    public Collection<OsmPrimitive> getSelectedChildPrimitives() {
525        Collection<OsmPrimitive> ret = new ArrayList<>();
526        for (RelationMember m: getSelectedMembers()) {
527            ret.add(m.getMember());
528        }
529        return ret;
530    }
531
532    /**
533     * Replies the set of selected referers. Never null, but may be empty.
534     * @param referenceSet reference set
535     *
536     * @return the set of selected referers
537     */
538    public Set<OsmPrimitive> getChildPrimitives(Collection<? extends OsmPrimitive> referenceSet) {
539        Set<OsmPrimitive> ret = new HashSet<>();
540        if (referenceSet == null) return null;
541        for (RelationMember m: members) {
542            if (referenceSet.contains(m.getMember())) {
543                ret.add(m.getMember());
544            }
545        }
546        return ret;
547    }
548
549    /**
550     * Selects the members in the collection selectedMembers
551     *
552     * @param selectedMembers the collection of selected members
553     */
554    public void setSelectedMembers(Collection<RelationMember> selectedMembers) {
555        if (selectedMembers == null || selectedMembers.isEmpty()) {
556            getSelectionModel().clearSelection();
557            return;
558        }
559
560        // lookup the indices for the respective members
561        //
562        Set<Integer> selectedIndices = new HashSet<>();
563        for (RelationMember member : selectedMembers) {
564            for (int idx = 0; idx < members.size(); ++idx) {
565                if (member.equals(members.get(idx))) {
566                    selectedIndices.add(idx);
567                }
568            }
569        }
570        setSelectedMembersIdx(selectedIndices);
571    }
572
573    /**
574     * Selects the members in the collection selectedIndices
575     *
576     * @param selectedIndices the collection of selected member indices
577     */
578    public void setSelectedMembersIdx(Collection<Integer> selectedIndices) {
579        if (selectedIndices == null || selectedIndices.isEmpty()) {
580            getSelectionModel().clearSelection();
581            return;
582        }
583        // select the members
584        //
585        getSelectionModel().setValueIsAdjusting(true);
586        getSelectionModel().clearSelection();
587        for (int row : selectedIndices) {
588            getSelectionModel().addSelectionInterval(row, row);
589        }
590        getSelectionModel().setValueIsAdjusting(false);
591        // make the first selected member visible
592        //
593        if (!selectedIndices.isEmpty()) {
594            fireMakeMemberVisible(Collections.min(selectedIndices));
595        }
596    }
597
598    /**
599     * Replies true if the index-th relation members referrs
600     * to an editable relation, i.e. a relation which is not
601     * incomplete.
602     *
603     * @param index the index
604     * @return true, if the index-th relation members referrs
605     * to an editable relation, i.e. a relation which is not
606     * incomplete
607     */
608    public boolean isEditableRelation(int index) {
609        if (index < 0 || index >= members.size())
610            return false;
611        RelationMember member = members.get(index);
612        if (!member.isRelation())
613            return false;
614        Relation r = member.getRelation();
615        return !r.isIncomplete();
616    }
617
618    /**
619     * Replies true if there is at least one relation member given as {@code members}
620     * which refers to at least on the primitives in {@code primitives}.
621     *
622     * @param members the members
623     * @param primitives the collection of primitives
624     * @return true if there is at least one relation member in this model
625     * which refers to at least on the primitives in <code>primitives</code>; false
626     * otherwise
627     */
628    public static boolean hasMembersReferringTo(Collection<RelationMember> members, Collection<OsmPrimitive> primitives) {
629        if (primitives == null || primitives.isEmpty())
630            return false;
631        Set<OsmPrimitive> referrers = new HashSet<>();
632        for (RelationMember member : members) {
633            referrers.add(member.getMember());
634        }
635        for (OsmPrimitive referred : primitives) {
636            if (referrers.contains(referred))
637                return true;
638        }
639        return false;
640    }
641
642    /**
643     * Replies true if there is at least one relation member in this model
644     * which refers to at least on the primitives in <code>primitives</code>.
645     *
646     * @param primitives the collection of primitives
647     * @return true if there is at least one relation member in this model
648     * which refers to at least on the primitives in <code>primitives</code>; false
649     * otherwise
650     */
651    public boolean hasMembersReferringTo(Collection<OsmPrimitive> primitives) {
652        return hasMembersReferringTo(members, primitives);
653    }
654
655    /**
656     * Selects all mebers which refer to {@link OsmPrimitive}s in the collections
657     * <code>primitmives</code>. Does nothing is primitives is null.
658     *
659     * @param primitives the collection of primitives
660     */
661    public void selectMembersReferringTo(Collection<? extends OsmPrimitive> primitives) {
662        if (primitives == null) return;
663        getSelectionModel().setValueIsAdjusting(true);
664        getSelectionModel().clearSelection();
665        for (int i = 0; i < members.size(); i++) {
666            RelationMember m = members.get(i);
667            if (primitives.contains(m.getMember())) {
668                this.getSelectionModel().addSelectionInterval(i, i);
669            }
670        }
671        getSelectionModel().setValueIsAdjusting(false);
672        if (!getSelectedIndices().isEmpty()) {
673            fireMakeMemberVisible(getSelectedIndices().get(0));
674        }
675    }
676
677    /**
678     * Replies true if <code>primitive</code> is currently selected in the layer this
679     * model is attached to
680     *
681     * @param primitive the primitive
682     * @return true if <code>primitive</code> is currently selected in the layer this
683     * model is attached to, false otherwise
684     */
685    public boolean isInJosmSelection(OsmPrimitive primitive) {
686        return layer.data.isSelected(primitive);
687    }
688
689    /**
690     * Sort the selected relation members by the way they are linked.
691     */
692    void sort() {
693        List<RelationMember> selectedMembers = new ArrayList<>(getSelectedMembers());
694        List<RelationMember> sortedMembers = null;
695        List<RelationMember> newMembers;
696        if (selectedMembers.size() <= 1) {
697            newMembers = relationSorter.sortMembers(members);
698            sortedMembers = newMembers;
699        } else {
700            sortedMembers = relationSorter.sortMembers(selectedMembers);
701            List<Integer> selectedIndices = getSelectedIndices();
702            newMembers = new ArrayList<>();
703            boolean inserted = false;
704            for (int i = 0; i < members.size(); i++) {
705                if (selectedIndices.contains(i)) {
706                    if (!inserted) {
707                        newMembers.addAll(sortedMembers);
708                        inserted = true;
709                    }
710                } else {
711                    newMembers.add(members.get(i));
712                }
713            }
714        }
715
716        if (members.size() != newMembers.size()) throw new AssertionError();
717
718        members.clear();
719        members.addAll(newMembers);
720        fireTableDataChanged();
721        setSelectedMembers(sortedMembers);
722    }
723
724    /**
725     * Sort the selected relation members and all members below by the way they are linked.
726     */
727    void sortBelow() {
728        final List<RelationMember> subList = members.subList(getSelectionModel().getMinSelectionIndex(), members.size());
729        final List<RelationMember> sorted = relationSorter.sortMembers(subList);
730        subList.clear();
731        subList.addAll(sorted);
732        fireTableDataChanged();
733        setSelectedMembers(sorted);
734    }
735
736    WayConnectionType getWayConnection(int i) {
737        if (connectionType == null) {
738            connectionType = wayConnectionTypeCalculator.updateLinks(members);
739        }
740        return connectionType.get(i);
741    }
742
743    @Override
744    public void tableChanged(TableModelEvent e) {
745        connectionType = null;
746    }
747
748    /**
749     * Reverse the relation members.
750     */
751    void reverse() {
752        List<Integer> selectedIndices = getSelectedIndices();
753        List<Integer> selectedIndicesReversed = getSelectedIndices();
754
755        if (selectedIndices.size() <= 1) {
756            Collections.reverse(members);
757            fireTableDataChanged();
758            setSelectedMembers(members);
759        } else {
760            Collections.reverse(selectedIndicesReversed);
761
762            List<RelationMember> newMembers = new ArrayList<>(members);
763
764            for (int i = 0; i < selectedIndices.size(); i++) {
765                newMembers.set(selectedIndices.get(i), members.get(selectedIndicesReversed.get(i)));
766            }
767
768            if (members.size() != newMembers.size()) throw new AssertionError();
769            members.clear();
770            members.addAll(newMembers);
771            fireTableDataChanged();
772            setSelectedMembersIdx(selectedIndices);
773        }
774    }
775}