001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.relation;
003
004import java.util.ArrayList;
005import java.util.BitSet;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.EnumSet;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Set;
012import java.util.TreeSet;
013import java.util.concurrent.CopyOnWriteArrayList;
014import java.util.stream.Collectors;
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.data.osm.DataSelectionListener;
023import org.openstreetmap.josm.data.osm.OsmPrimitive;
024import org.openstreetmap.josm.data.osm.Relation;
025import org.openstreetmap.josm.data.osm.RelationMember;
026import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
027import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
028import org.openstreetmap.josm.data.osm.event.DataSetListener;
029import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
030import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
031import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
032import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
033import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
034import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
035import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
036import org.openstreetmap.josm.gui.MainApplication;
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.util.SortableTableModel;
047import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
048import org.openstreetmap.josm.tools.ArrayUtils;
049import org.openstreetmap.josm.tools.JosmRuntimeException;
050import org.openstreetmap.josm.tools.bugreport.BugReport;
051
052/**
053 * This is the base model used for the {@link MemberTable}. It holds the member data.
054 */
055public class MemberTableModel extends AbstractTableModel
056implements TableModelListener, DataSelectionListener, DataSetListener, OsmPrimitivesTableModel, SortableTableModel<RelationMember> {
057
058    /**
059     * data of the table model: The list of members and the cached WayConnectionType of each member.
060     **/
061    private final transient List<RelationMember> members;
062    private transient List<WayConnectionType> connectionType;
063    private final transient Relation relation;
064
065    private DefaultListSelectionModel listSelectionModel;
066    private final transient CopyOnWriteArrayList<IMemberModelListener> listeners;
067    private final transient OsmDataLayer layer;
068    private final transient TaggingPresetHandler presetHandler;
069
070    private final transient WayConnectionTypeCalculator wayConnectionTypeCalculator = new WayConnectionTypeCalculator();
071    private final transient RelationSorter relationSorter = new RelationSorter();
072
073    /**
074     * constructor
075     * @param relation relation
076     * @param layer data layer
077     * @param presetHandler tagging preset handler
078     */
079    public MemberTableModel(Relation relation, OsmDataLayer layer, TaggingPresetHandler presetHandler) {
080        this.relation = relation;
081        this.members = new ArrayList<>();
082        this.listeners = new CopyOnWriteArrayList<>();
083        this.layer = layer;
084        this.presetHandler = presetHandler;
085        addTableModelListener(this);
086    }
087
088    /**
089     * Returns the data layer.
090     * @return the data layer
091     */
092    public OsmDataLayer getLayer() {
093        return layer;
094    }
095
096    /**
097     * Registers listeners (selection change and dataset change).
098     */
099    public void register() {
100        SelectionEventManager.getInstance().addSelectionListener(this);
101        getLayer().data.addDataSetListener(this);
102    }
103
104    /**
105     * Unregisters listeners (selection change and dataset change).
106     */
107    public void unregister() {
108        SelectionEventManager.getInstance().removeSelectionListener(this);
109        getLayer().data.removeDataSetListener(this);
110    }
111
112    /* --------------------------------------------------------------------------- */
113    /* Interface DataSelectionListener                                             */
114    /* --------------------------------------------------------------------------- */
115    @Override
116    public void selectionChanged(SelectionChangeEvent event) {
117        if (MainApplication.getLayerManager().getActiveDataLayer() != this.layer) return;
118        // just trigger a repaint
119        Collection<RelationMember> sel = getSelectedMembers();
120        fireTableDataChanged();
121        setSelectedMembers(sel);
122    }
123
124    /* --------------------------------------------------------------------------- */
125    /* Interface DataSetListener                                                   */
126    /* --------------------------------------------------------------------------- */
127    @Override
128    public void dataChanged(DataChangedEvent event) {
129        // just trigger a repaint - the display name of the relation members may have changed
130        Collection<RelationMember> sel = getSelectedMembers();
131        GuiHelper.runInEDT(this::fireTableDataChanged);
132        setSelectedMembers(sel);
133    }
134
135    @Override
136    public void nodeMoved(NodeMovedEvent event) {
137        // ignore
138    }
139
140    @Override
141    public void primitivesAdded(PrimitivesAddedEvent event) {
142        // ignore
143    }
144
145    @Override
146    public void primitivesRemoved(PrimitivesRemovedEvent event) {
147        // ignore - the relation in the editor might become out of sync with the relation
148        // in the dataset. We will deal with it when the relation editor is closed or
149        // when the changes in the editor are applied.
150    }
151
152    @Override
153    public void relationMembersChanged(RelationMembersChangedEvent event) {
154        // ignore - the relation in the editor might become out of sync with the relation
155        // in the dataset. We will deal with it when the relation editor is closed or
156        // when the changes in the editor are applied.
157    }
158
159    @Override
160    public void tagsChanged(TagsChangedEvent event) {
161        // just refresh the respective table cells
162        //
163        Collection<RelationMember> sel = getSelectedMembers();
164        for (int i = 0; i < members.size(); i++) {
165            if (members.get(i).getMember() == event.getPrimitive()) {
166                fireTableCellUpdated(i, 1 /* the column with the primitive name */);
167            }
168        }
169        setSelectedMembers(sel);
170    }
171
172    @Override
173    public void wayNodesChanged(WayNodesChangedEvent event) {
174        // ignore
175    }
176
177    @Override
178    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
179        // ignore
180    }
181
182    /* --------------------------------------------------------------------------- */
183
184    /**
185     * Add a new member model listener.
186     * @param listener member model listener to add
187     */
188    public void addMemberModelListener(IMemberModelListener listener) {
189        if (listener != null) {
190            listeners.addIfAbsent(listener);
191        }
192    }
193
194    /**
195     * Remove a member model listener.
196     * @param listener member model listener to remove
197     */
198    public void removeMemberModelListener(IMemberModelListener listener) {
199        listeners.remove(listener);
200    }
201
202    protected void fireMakeMemberVisible(int index) {
203        for (IMemberModelListener listener : listeners) {
204            listener.makeMemberVisible(index);
205        }
206    }
207
208    /**
209     * Populates this model from the given relation.
210     * @param relation relation
211     */
212    public void populate(Relation relation) {
213        members.clear();
214        if (relation != null) {
215            // make sure we work with clones of the relation members in the model.
216            members.addAll(new Relation(relation).getMembers());
217        }
218        fireTableDataChanged();
219    }
220
221    @Override
222    public int getColumnCount() {
223        return 3;
224    }
225
226    @Override
227    public int getRowCount() {
228        return members.size();
229    }
230
231    @Override
232    public Object getValueAt(int rowIndex, int columnIndex) {
233        switch (columnIndex) {
234        case 0:
235            return members.get(rowIndex).getRole();
236        case 1:
237            return members.get(rowIndex).getMember();
238        case 2:
239            return getWayConnection(rowIndex);
240        }
241        // should not happen
242        return null;
243    }
244
245    @Override
246    public boolean isCellEditable(int rowIndex, int columnIndex) {
247        return columnIndex == 0;
248    }
249
250    @Override
251    public void setValueAt(Object value, int rowIndex, int columnIndex) {
252        // fix #10524 - IndexOutOfBoundsException: Index: 2, Size: 2
253        if (rowIndex >= members.size()) {
254            return;
255        }
256        RelationMember member = members.get(rowIndex);
257        String role = value.toString();
258        if (member.hasRole(role))
259            return;
260        RelationMember newMember = new RelationMember(role, member.getMember());
261        members.remove(rowIndex);
262        members.add(rowIndex, newMember);
263        fireTableDataChanged();
264    }
265
266    @Override
267    public OsmPrimitive getReferredPrimitive(int idx) {
268        return members.get(idx).getMember();
269    }
270
271    @Override
272    public boolean move(int delta, int... selectedRows) {
273        if (!canMove(delta, this::getRowCount, selectedRows))
274            return false;
275        doMove(delta, selectedRows);
276        fireTableDataChanged();
277        final ListSelectionModel selectionModel = getSelectionModel();
278        selectionModel.setValueIsAdjusting(true);
279        selectionModel.clearSelection();
280        BitSet selected = new BitSet();
281        for (int row : selectedRows) {
282            row += delta;
283            selected.set(row);
284        }
285        addToSelectedMembers(selected);
286        selectionModel.setValueIsAdjusting(false);
287        fireMakeMemberVisible(selectedRows[0] + delta);
288        return true;
289    }
290
291    /**
292     * Remove selected rows, if possible.
293     * @param selectedRows rows to remove
294     * @see #canRemove
295     */
296    public void remove(int... selectedRows) {
297        if (!canRemove(selectedRows))
298            return;
299        int offset = 0;
300        for (int row : selectedRows) {
301            row -= offset;
302            if (members.size() > row) {
303                members.remove(row);
304                getSelectionModel().removeIndexInterval(row, row);
305                offset++;
306            }
307        }
308        fireTableDataChanged();
309    }
310
311    /**
312     * Checks that a range of rows can be removed.
313     * @param rows indexes of rows to remove
314     * @return {@code true} if rows can be removed
315     */
316    public boolean canRemove(int... rows) {
317        return rows != null && rows.length != 0;
318    }
319
320    @Override
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    @Override
330    public RelationMember getValue(int index) {
331        return members.get(index);
332    }
333
334    @Override
335    public RelationMember setValue(int index, RelationMember value) {
336        return members.set(index, value);
337    }
338
339    /**
340     * Remove members referring to the given list of primitives.
341     * @param primitives list of OSM primitives
342     */
343    public void removeMembersReferringTo(List<? extends OsmPrimitive> primitives) {
344        if (primitives == null)
345            return;
346        if (members.removeIf(member -> primitives.contains(member.getMember())))
347            fireTableDataChanged();
348    }
349
350    /**
351     * Applies this member model to the given relation.
352     * @param relation relation
353     */
354    public void applyToRelation(Relation relation) {
355        relation.setMembers(members.stream()
356                .filter(rm -> !rm.getMember().isDeleted()).collect(Collectors.toList()));
357    }
358
359    /**
360     * Determines if this model has the same members as the given relation.
361     * @param relation relation
362     * @return {@code true} if this model has the same members as {@code relation}
363     */
364    public boolean hasSameMembersAs(Relation relation) {
365        if (relation == null || relation.getMembersCount() != members.size())
366            return false;
367        for (int i = 0; i < relation.getMembersCount(); i++) {
368            if (!relation.getMember(i).equals(members.get(i)))
369                return false;
370        }
371        return true;
372    }
373
374    /**
375     * Replies the set of incomplete primitives
376     *
377     * @return the set of incomplete primitives
378     */
379    public Set<OsmPrimitive> getIncompleteMemberPrimitives() {
380        Set<OsmPrimitive> ret = new HashSet<>();
381        for (RelationMember member : members) {
382            if (member.getMember().isIncomplete()) {
383                ret.add(member.getMember());
384            }
385        }
386        return ret;
387    }
388
389    /**
390     * Replies the set of selected incomplete primitives
391     *
392     * @return the set of selected incomplete primitives
393     */
394    public Set<OsmPrimitive> getSelectedIncompleteMemberPrimitives() {
395        Set<OsmPrimitive> ret = new HashSet<>();
396        for (RelationMember member : getSelectedMembers()) {
397            if (member.getMember().isIncomplete()) {
398                ret.add(member.getMember());
399            }
400        }
401        return ret;
402    }
403
404    /**
405     * Replies true if at least one the relation members is incomplete
406     *
407     * @return true if at least one the relation members is incomplete
408     */
409    public boolean hasIncompleteMembers() {
410        for (RelationMember member : members) {
411            if (member.getMember().isIncomplete())
412                return true;
413        }
414        return false;
415    }
416
417    /**
418     * Replies true if at least one of the selected members is incomplete
419     *
420     * @return true if at least one of the selected members is incomplete
421     */
422    public boolean hasIncompleteSelectedMembers() {
423        for (RelationMember member : getSelectedMembers()) {
424            if (member.getMember().isIncomplete())
425                return true;
426        }
427        return false;
428    }
429
430    private void addMembersAtIndex(List<? extends OsmPrimitive> primitives, int index) {
431        if (primitives == null || primitives.isEmpty())
432            return;
433        int idx = index;
434        for (OsmPrimitive primitive : primitives) {
435            final RelationMember member = getRelationMemberForPrimitive(primitive);
436            members.add(idx++, member);
437        }
438        fireTableDataChanged();
439        getSelectionModel().clearSelection();
440        getSelectionModel().addSelectionInterval(index, index + primitives.size() - 1);
441        fireMakeMemberVisible(index);
442    }
443
444    RelationMember getRelationMemberForPrimitive(final OsmPrimitive primitive) {
445        final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
446                EnumSet.of(relation != null ? TaggingPresetType.forPrimitive(relation) : TaggingPresetType.RELATION),
447                presetHandler.getSelection().iterator().next().getKeys(), false);
448        Collection<String> potentialRoles = new TreeSet<>();
449        for (TaggingPreset tp : presets) {
450            String suggestedRole = tp.suggestRoleForOsmPrimitive(primitive);
451            if (suggestedRole != null) {
452                potentialRoles.add(suggestedRole);
453            }
454        }
455        // TODO: propose user to choose role among potential ones instead of picking first one
456        final String role = potentialRoles.isEmpty() ? "" : potentialRoles.iterator().next();
457        return new RelationMember(role == null ? "" : role, primitive);
458    }
459
460    void addMembersAtIndexKeepingOldSelection(final Iterable<RelationMember> newMembers, final int index) {
461        int idx = index;
462        for (RelationMember member : newMembers) {
463            members.add(idx++, member);
464        }
465        invalidateConnectionType();
466        fireTableRowsInserted(index, idx - 1);
467    }
468
469    public void addMembersAtBeginning(List<? extends OsmPrimitive> primitives) {
470        addMembersAtIndex(primitives, 0);
471    }
472
473    public void addMembersAtEnd(List<? extends OsmPrimitive> primitives) {
474        addMembersAtIndex(primitives, members.size());
475    }
476
477    public void addMembersBeforeIdx(List<? extends OsmPrimitive> primitives, int idx) {
478        addMembersAtIndex(primitives, idx);
479    }
480
481    public void addMembersAfterIdx(List<? extends OsmPrimitive> primitives, int idx) {
482        addMembersAtIndex(primitives, idx + 1);
483    }
484
485    /**
486     * Replies the number of members which refer to a particular primitive
487     *
488     * @param primitive the primitive
489     * @return the number of members which refer to a particular primitive
490     */
491    public int getNumMembersWithPrimitive(OsmPrimitive primitive) {
492        int count = 0;
493        for (RelationMember member : members) {
494            if (member.getMember().equals(primitive)) {
495                count++;
496            }
497        }
498        return count;
499    }
500
501    /**
502     * updates the role of the members given by the indices in <code>idx</code>
503     *
504     * @param idx the array of indices
505     * @param role the new role
506     */
507    public void updateRole(int[] idx, String role) {
508        if (idx == null || idx.length == 0)
509            return;
510        for (int row : idx) {
511            // fix #7885 - IndexOutOfBoundsException: Index: 39, Size: 39
512            if (row >= members.size()) {
513                continue;
514            }
515            RelationMember oldMember = members.get(row);
516            RelationMember newMember = new RelationMember(role, oldMember.getMember());
517            members.remove(row);
518            members.add(row, newMember);
519        }
520        fireTableDataChanged();
521        BitSet selected = new BitSet();
522        for (int row : idx) {
523            selected.set(row);
524        }
525        addToSelectedMembers(selected);
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        BitSet selected = new BitSet();
610        for (int row : selectedIndices) {
611            selected.set(row);
612        }
613        addToSelectedMembers(selected);
614        getSelectionModel().setValueIsAdjusting(false);
615        // make the first selected member visible
616        //
617        if (!selectedIndices.isEmpty()) {
618            fireMakeMemberVisible(Collections.min(selectedIndices));
619        }
620    }
621
622    /**
623     * Add one or more members indices to the selection.
624     * Detect groups of consecutive indices.
625     * Only one costly call of addSelectionInterval is performed for each group
626
627     * @param selectedIndices selected indices as a bitset
628     * @return number of groups
629     */
630    private int addToSelectedMembers(BitSet selectedIndices) {
631        if (selectedIndices == null || selectedIndices.isEmpty()) {
632            return 0;
633        }
634        // select the members
635        //
636        int start = selectedIndices.nextSetBit(0);
637        int end;
638        int steps = 0;
639        int last = selectedIndices.length();
640        while (start >= 0) {
641            end = selectedIndices.nextClearBit(start);
642            steps++;
643            getSelectionModel().addSelectionInterval(start, end-1);
644            start = selectedIndices.nextSetBit(end);
645            if (start < 0 || end == last)
646                break;
647        }
648        return steps;
649    }
650
651    /**
652     * Replies true if the index-th relation members refers
653     * to an editable relation, i.e. a relation which is not
654     * incomplete.
655     *
656     * @param index the index
657     * @return true, if the index-th relation members refers
658     * to an editable relation, i.e. a relation which is not
659     * incomplete
660     */
661    public boolean isEditableRelation(int index) {
662        if (index < 0 || index >= members.size())
663            return false;
664        RelationMember member = members.get(index);
665        if (!member.isRelation())
666            return false;
667        Relation r = member.getRelation();
668        return !r.isIncomplete();
669    }
670
671    /**
672     * Replies true if there is at least one relation member given as {@code members}
673     * which refers to at least on the primitives in {@code primitives}.
674     *
675     * @param members the members
676     * @param primitives the collection of primitives
677     * @return true if there is at least one relation member in this model
678     * which refers to at least on the primitives in <code>primitives</code>; false
679     * otherwise
680     */
681    public static boolean hasMembersReferringTo(Collection<RelationMember> members, Collection<OsmPrimitive> primitives) {
682        if (primitives == null || primitives.isEmpty())
683            return false;
684        Set<OsmPrimitive> referrers = new HashSet<>();
685        for (RelationMember member : members) {
686            referrers.add(member.getMember());
687        }
688        for (OsmPrimitive referred : primitives) {
689            if (referrers.contains(referred))
690                return true;
691        }
692        return false;
693    }
694
695    /**
696     * Replies true if there is at least one relation member in this model
697     * which refers to at least on the primitives in <code>primitives</code>.
698     *
699     * @param primitives the collection of primitives
700     * @return true if there is at least one relation member in this model
701     * which refers to at least on the primitives in <code>primitives</code>; false
702     * otherwise
703     */
704    public boolean hasMembersReferringTo(Collection<OsmPrimitive> primitives) {
705        return hasMembersReferringTo(members, primitives);
706    }
707
708    /**
709     * Selects all members which refer to {@link OsmPrimitive}s in the collections
710     * <code>primitmives</code>. Does nothing is primitives is null.
711     *
712     * @param primitives the collection of primitives
713     */
714    public void selectMembersReferringTo(Collection<? extends OsmPrimitive> primitives) {
715        if (primitives == null) return;
716        getSelectionModel().setValueIsAdjusting(true);
717        getSelectionModel().clearSelection();
718        BitSet selected = new BitSet();
719        for (int i = 0; i < members.size(); i++) {
720            RelationMember m = members.get(i);
721            if (primitives.contains(m.getMember())) {
722                selected.set(i);
723            }
724        }
725        addToSelectedMembers(selected);
726        getSelectionModel().setValueIsAdjusting(false);
727        int[] selectedIndices = getSelectedIndices();
728        if (selectedIndices.length > 0) {
729            fireMakeMemberVisible(selectedIndices[0]);
730        }
731    }
732
733    /**
734     * Replies true if <code>primitive</code> is currently selected in the layer this
735     * model is attached to
736     *
737     * @param primitive the primitive
738     * @return true if <code>primitive</code> is currently selected in the layer this
739     * model is attached to, false otherwise
740     */
741    public boolean isInJosmSelection(OsmPrimitive primitive) {
742        return layer.data.isSelected(primitive);
743    }
744
745    /**
746     * Sort the selected relation members by the way they are linked.
747     */
748    @Override
749    public void sort() {
750        List<RelationMember> selectedMembers = new ArrayList<>(getSelectedMembers());
751        List<RelationMember> sortedMembers;
752        List<RelationMember> newMembers;
753        if (selectedMembers.size() <= 1) {
754            newMembers = relationSorter.sortMembers(members);
755            sortedMembers = newMembers;
756        } else {
757            sortedMembers = relationSorter.sortMembers(selectedMembers);
758            List<Integer> selectedIndices = ArrayUtils.toList(getSelectedIndices());
759            newMembers = new ArrayList<>();
760            boolean inserted = false;
761            for (int i = 0; i < members.size(); i++) {
762                if (selectedIndices.contains(i)) {
763                    if (!inserted) {
764                        newMembers.addAll(sortedMembers);
765                        inserted = true;
766                    }
767                } else {
768                    newMembers.add(members.get(i));
769                }
770            }
771        }
772
773        if (members.size() != newMembers.size())
774            throw new AssertionError();
775
776        members.clear();
777        members.addAll(newMembers);
778        fireTableDataChanged();
779        setSelectedMembers(sortedMembers);
780    }
781
782    /**
783     * Sort the selected relation members and all members below by the way they are linked.
784     */
785    public void sortBelow() {
786        final List<RelationMember> subList = members.subList(Math.max(0, getSelectionModel().getMinSelectionIndex()), members.size());
787        final List<RelationMember> sorted = relationSorter.sortMembers(subList);
788        subList.clear();
789        subList.addAll(sorted);
790        fireTableDataChanged();
791        setSelectedMembers(sorted);
792    }
793
794    WayConnectionType getWayConnection(int i) {
795        try {
796            if (connectionType == null) {
797                connectionType = wayConnectionTypeCalculator.updateLinks(members);
798            }
799            return connectionType.get(i);
800        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
801            throw BugReport.intercept(e).put("i", i).put("members", members).put("relation", relation);
802        }
803    }
804
805    @Override
806    public void tableChanged(TableModelEvent e) {
807        invalidateConnectionType();
808    }
809
810    private void invalidateConnectionType() {
811        connectionType = null;
812    }
813
814    /**
815     * Reverse the relation members.
816     */
817    @Override
818    public void reverse() {
819        List<Integer> selectedIndices = ArrayUtils.toList(getSelectedIndices());
820        List<Integer> selectedIndicesReversed = ArrayUtils.toList(getSelectedIndices());
821
822        if (selectedIndices.size() <= 1) {
823            Collections.reverse(members);
824            fireTableDataChanged();
825            setSelectedMembers(members);
826        } else {
827            Collections.reverse(selectedIndicesReversed);
828
829            List<RelationMember> newMembers = new ArrayList<>(members);
830
831            for (int i = 0; i < selectedIndices.size(); i++) {
832                newMembers.set(selectedIndices.get(i), members.get(selectedIndicesReversed.get(i)));
833            }
834
835            if (members.size() != newMembers.size()) throw new AssertionError();
836            members.clear();
837            members.addAll(newMembers);
838            fireTableDataChanged();
839            setSelectedMembersIdx(selectedIndices);
840        }
841    }
842}