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