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        final ListSelectionModel selectionModel = getSelectionModel();
301        selectionModel.setValueIsAdjusting(true);
302        for (int row : selectedRows) {
303            row -= offset;
304            if (members.size() > row) {
305                members.remove(row);
306                selectionModel.removeIndexInterval(row, row);
307                offset++;
308            }
309        }
310        selectionModel.setValueIsAdjusting(false);
311        fireTableDataChanged();
312    }
313
314    /**
315     * Checks that a range of rows can be removed.
316     * @param rows indexes of rows to remove
317     * @return {@code true} if rows can be removed
318     */
319    public boolean canRemove(int... rows) {
320        return rows != null && rows.length != 0;
321    }
322
323    @Override
324    public DefaultListSelectionModel getSelectionModel() {
325        if (listSelectionModel == null) {
326            listSelectionModel = new DefaultListSelectionModel();
327            listSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
328        }
329        return listSelectionModel;
330    }
331
332    @Override
333    public RelationMember getValue(int index) {
334        return members.get(index);
335    }
336
337    @Override
338    public RelationMember setValue(int index, RelationMember value) {
339        return members.set(index, value);
340    }
341
342    /**
343     * Remove members referring to the given list of primitives.
344     * @param primitives list of OSM primitives
345     */
346    public void removeMembersReferringTo(List<? extends OsmPrimitive> primitives) {
347        if (primitives == null)
348            return;
349        if (members.removeIf(member -> primitives.contains(member.getMember())))
350            fireTableDataChanged();
351    }
352
353    /**
354     * Applies this member model to the given relation.
355     * @param relation relation
356     */
357    public void applyToRelation(Relation relation) {
358        relation.setMembers(members.stream()
359                .filter(rm -> !rm.getMember().isDeleted()).collect(Collectors.toList()));
360    }
361
362    /**
363     * Determines if this model has the same members as the given relation.
364     * @param relation relation
365     * @return {@code true} if this model has the same members as {@code relation}
366     */
367    public boolean hasSameMembersAs(Relation relation) {
368        if (relation == null || relation.getMembersCount() != members.size())
369            return false;
370        for (int i = 0; i < relation.getMembersCount(); i++) {
371            if (!relation.getMember(i).equals(members.get(i)))
372                return false;
373        }
374        return true;
375    }
376
377    /**
378     * Replies the set of incomplete primitives
379     *
380     * @return the set of incomplete primitives
381     */
382    public Set<OsmPrimitive> getIncompleteMemberPrimitives() {
383        Set<OsmPrimitive> ret = new HashSet<>();
384        for (RelationMember member : members) {
385            if (member.getMember().isIncomplete()) {
386                ret.add(member.getMember());
387            }
388        }
389        return ret;
390    }
391
392    /**
393     * Replies the set of selected incomplete primitives
394     *
395     * @return the set of selected incomplete primitives
396     */
397    public Set<OsmPrimitive> getSelectedIncompleteMemberPrimitives() {
398        Set<OsmPrimitive> ret = new HashSet<>();
399        for (RelationMember member : getSelectedMembers()) {
400            if (member.getMember().isIncomplete()) {
401                ret.add(member.getMember());
402            }
403        }
404        return ret;
405    }
406
407    /**
408     * Replies true if at least one the relation members is incomplete
409     *
410     * @return true if at least one the relation members is incomplete
411     */
412    public boolean hasIncompleteMembers() {
413        for (RelationMember member : members) {
414            if (member.getMember().isIncomplete())
415                return true;
416        }
417        return false;
418    }
419
420    /**
421     * Replies true if at least one of the selected members is incomplete
422     *
423     * @return true if at least one of the selected members is incomplete
424     */
425    public boolean hasIncompleteSelectedMembers() {
426        for (RelationMember member : getSelectedMembers()) {
427            if (member.getMember().isIncomplete())
428                return true;
429        }
430        return false;
431    }
432
433    private void addMembersAtIndex(List<? extends OsmPrimitive> primitives, int index) {
434        if (primitives == null || primitives.isEmpty())
435            return;
436        int idx = index;
437        for (OsmPrimitive primitive : primitives) {
438            final RelationMember member = getRelationMemberForPrimitive(primitive);
439            members.add(idx++, member);
440        }
441        fireTableDataChanged();
442        getSelectionModel().clearSelection();
443        getSelectionModel().addSelectionInterval(index, index + primitives.size() - 1);
444        fireMakeMemberVisible(index);
445    }
446
447    RelationMember getRelationMemberForPrimitive(final OsmPrimitive primitive) {
448        final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
449                EnumSet.of(relation != null ? TaggingPresetType.forPrimitive(relation) : TaggingPresetType.RELATION),
450                presetHandler.getSelection().iterator().next().getKeys(), false);
451        Collection<String> potentialRoles = new TreeSet<>();
452        for (TaggingPreset tp : presets) {
453            String suggestedRole = tp.suggestRoleForOsmPrimitive(primitive);
454            if (suggestedRole != null) {
455                potentialRoles.add(suggestedRole);
456            }
457        }
458        // TODO: propose user to choose role among potential ones instead of picking first one
459        final String role = potentialRoles.isEmpty() ? "" : potentialRoles.iterator().next();
460        return new RelationMember(role == null ? "" : role, primitive);
461    }
462
463    void addMembersAtIndexKeepingOldSelection(final Iterable<RelationMember> newMembers, final int index) {
464        int idx = index;
465        for (RelationMember member : newMembers) {
466            members.add(idx++, member);
467        }
468        invalidateConnectionType();
469        fireTableRowsInserted(index, idx - 1);
470    }
471
472    public void addMembersAtBeginning(List<? extends OsmPrimitive> primitives) {
473        addMembersAtIndex(primitives, 0);
474    }
475
476    public void addMembersAtEnd(List<? extends OsmPrimitive> primitives) {
477        addMembersAtIndex(primitives, members.size());
478    }
479
480    public void addMembersBeforeIdx(List<? extends OsmPrimitive> primitives, int idx) {
481        addMembersAtIndex(primitives, idx);
482    }
483
484    public void addMembersAfterIdx(List<? extends OsmPrimitive> primitives, int idx) {
485        addMembersAtIndex(primitives, idx + 1);
486    }
487
488    /**
489     * Replies the number of members which refer to a particular primitive
490     *
491     * @param primitive the primitive
492     * @return the number of members which refer to a particular primitive
493     */
494    public int getNumMembersWithPrimitive(OsmPrimitive primitive) {
495        int count = 0;
496        for (RelationMember member : members) {
497            if (member.getMember().equals(primitive)) {
498                count++;
499            }
500        }
501        return count;
502    }
503
504    /**
505     * updates the role of the members given by the indices in <code>idx</code>
506     *
507     * @param idx the array of indices
508     * @param role the new role
509     */
510    public void updateRole(int[] idx, String role) {
511        if (idx == null || idx.length == 0)
512            return;
513        for (int row : idx) {
514            // fix #7885 - IndexOutOfBoundsException: Index: 39, Size: 39
515            if (row >= members.size()) {
516                continue;
517            }
518            RelationMember oldMember = members.get(row);
519            RelationMember newMember = new RelationMember(role, oldMember.getMember());
520            members.remove(row);
521            members.add(row, newMember);
522        }
523        fireTableDataChanged();
524        BitSet selected = new BitSet();
525        for (int row : idx) {
526            selected.set(row);
527        }
528        addToSelectedMembers(selected);
529    }
530
531    /**
532     * Get the currently selected relation members
533     *
534     * @return a collection with the currently selected relation members
535     */
536    public Collection<RelationMember> getSelectedMembers() {
537        List<RelationMember> selectedMembers = new ArrayList<>();
538        for (int i : getSelectedIndices()) {
539            selectedMembers.add(members.get(i));
540        }
541        return selectedMembers;
542    }
543
544    /**
545     * Replies the set of selected referers. Never null, but may be empty.
546     *
547     * @return the set of selected referers
548     */
549    public Collection<OsmPrimitive> getSelectedChildPrimitives() {
550        Collection<OsmPrimitive> ret = new ArrayList<>();
551        for (RelationMember m: getSelectedMembers()) {
552            ret.add(m.getMember());
553        }
554        return ret;
555    }
556
557    /**
558     * Replies the set of selected referers. Never null, but may be empty.
559     * @param referenceSet reference set
560     *
561     * @return the set of selected referers
562     */
563    public Set<OsmPrimitive> getChildPrimitives(Collection<? extends OsmPrimitive> referenceSet) {
564        Set<OsmPrimitive> ret = new HashSet<>();
565        if (referenceSet == null) return null;
566        for (RelationMember m: members) {
567            if (referenceSet.contains(m.getMember())) {
568                ret.add(m.getMember());
569            }
570        }
571        return ret;
572    }
573
574    /**
575     * Selects the members in the collection selectedMembers
576     *
577     * @param selectedMembers the collection of selected members
578     */
579    public void setSelectedMembers(Collection<RelationMember> selectedMembers) {
580        if (selectedMembers == null || selectedMembers.isEmpty()) {
581            getSelectionModel().clearSelection();
582            return;
583        }
584
585        // lookup the indices for the respective members
586        //
587        Set<Integer> selectedIndices = new HashSet<>();
588        for (RelationMember member : selectedMembers) {
589            for (int idx = 0; idx < members.size(); ++idx) {
590                if (member.equals(members.get(idx))) {
591                    selectedIndices.add(idx);
592                }
593            }
594        }
595        setSelectedMembersIdx(selectedIndices);
596    }
597
598    /**
599     * Selects the members in the collection selectedIndices
600     *
601     * @param selectedIndices the collection of selected member indices
602     */
603    public void setSelectedMembersIdx(Collection<Integer> selectedIndices) {
604        if (selectedIndices == null || selectedIndices.isEmpty()) {
605            getSelectionModel().clearSelection();
606            return;
607        }
608        // select the members
609        //
610        getSelectionModel().setValueIsAdjusting(true);
611        getSelectionModel().clearSelection();
612        BitSet selected = new BitSet();
613        for (int row : selectedIndices) {
614            selected.set(row);
615        }
616        addToSelectedMembers(selected);
617        getSelectionModel().setValueIsAdjusting(false);
618        // make the first selected member visible
619        //
620        if (!selectedIndices.isEmpty()) {
621            fireMakeMemberVisible(Collections.min(selectedIndices));
622        }
623    }
624
625    /**
626     * Add one or more members indices to the selection.
627     * Detect groups of consecutive indices.
628     * Only one costly call of addSelectionInterval is performed for each group
629
630     * @param selectedIndices selected indices as a bitset
631     * @return number of groups
632     */
633    private int addToSelectedMembers(BitSet selectedIndices) {
634        if (selectedIndices == null || selectedIndices.isEmpty()) {
635            return 0;
636        }
637        // select the members
638        //
639        int start = selectedIndices.nextSetBit(0);
640        int end;
641        int steps = 0;
642        int last = selectedIndices.length();
643        while (start >= 0) {
644            end = selectedIndices.nextClearBit(start);
645            steps++;
646            getSelectionModel().addSelectionInterval(start, end-1);
647            start = selectedIndices.nextSetBit(end);
648            if (start < 0 || end == last)
649                break;
650        }
651        return steps;
652    }
653
654    /**
655     * Replies true if the index-th relation members refers
656     * to an editable relation, i.e. a relation which is not
657     * incomplete.
658     *
659     * @param index the index
660     * @return true, if the index-th relation members refers
661     * to an editable relation, i.e. a relation which is not
662     * incomplete
663     */
664    public boolean isEditableRelation(int index) {
665        if (index < 0 || index >= members.size())
666            return false;
667        RelationMember member = members.get(index);
668        if (!member.isRelation())
669            return false;
670        Relation r = member.getRelation();
671        return !r.isIncomplete();
672    }
673
674    /**
675     * Replies true if there is at least one relation member given as {@code members}
676     * which refers to at least on the primitives in {@code primitives}.
677     *
678     * @param members the members
679     * @param primitives the collection of primitives
680     * @return true if there is at least one relation member in this model
681     * which refers to at least on the primitives in <code>primitives</code>; false
682     * otherwise
683     */
684    public static boolean hasMembersReferringTo(Collection<RelationMember> members, Collection<OsmPrimitive> primitives) {
685        if (primitives == null || primitives.isEmpty())
686            return false;
687        Set<OsmPrimitive> referrers = new HashSet<>();
688        for (RelationMember member : members) {
689            referrers.add(member.getMember());
690        }
691        for (OsmPrimitive referred : primitives) {
692            if (referrers.contains(referred))
693                return true;
694        }
695        return false;
696    }
697
698    /**
699     * Replies true if there is at least one relation member in this model
700     * which refers to at least on the primitives in <code>primitives</code>.
701     *
702     * @param primitives the collection of primitives
703     * @return true if there is at least one relation member in this model
704     * which refers to at least on the primitives in <code>primitives</code>; false
705     * otherwise
706     */
707    public boolean hasMembersReferringTo(Collection<OsmPrimitive> primitives) {
708        return hasMembersReferringTo(members, primitives);
709    }
710
711    /**
712     * Selects all members which refer to {@link OsmPrimitive}s in the collections
713     * <code>primitmives</code>. Does nothing is primitives is null.
714     *
715     * @param primitives the collection of primitives
716     */
717    public void selectMembersReferringTo(Collection<? extends OsmPrimitive> primitives) {
718        if (primitives == null) return;
719        getSelectionModel().setValueIsAdjusting(true);
720        getSelectionModel().clearSelection();
721        BitSet selected = new BitSet();
722        for (int i = 0; i < members.size(); i++) {
723            RelationMember m = members.get(i);
724            if (primitives.contains(m.getMember())) {
725                selected.set(i);
726            }
727        }
728        addToSelectedMembers(selected);
729        getSelectionModel().setValueIsAdjusting(false);
730        int[] selectedIndices = getSelectedIndices();
731        if (selectedIndices.length > 0) {
732            fireMakeMemberVisible(selectedIndices[0]);
733        }
734    }
735
736    /**
737     * Replies true if <code>primitive</code> is currently selected in the layer this
738     * model is attached to
739     *
740     * @param primitive the primitive
741     * @return true if <code>primitive</code> is currently selected in the layer this
742     * model is attached to, false otherwise
743     */
744    public boolean isInJosmSelection(OsmPrimitive primitive) {
745        return layer.data.isSelected(primitive);
746    }
747
748    /**
749     * Sort the selected relation members by the way they are linked.
750     */
751    @Override
752    public void sort() {
753        List<RelationMember> selectedMembers = new ArrayList<>(getSelectedMembers());
754        List<RelationMember> sortedMembers;
755        List<RelationMember> newMembers;
756        if (selectedMembers.size() <= 1) {
757            newMembers = relationSorter.sortMembers(members);
758            sortedMembers = newMembers;
759        } else {
760            sortedMembers = relationSorter.sortMembers(selectedMembers);
761            List<Integer> selectedIndices = ArrayUtils.toList(getSelectedIndices());
762            newMembers = new ArrayList<>();
763            boolean inserted = false;
764            for (int i = 0; i < members.size(); i++) {
765                if (selectedIndices.contains(i)) {
766                    if (!inserted) {
767                        newMembers.addAll(sortedMembers);
768                        inserted = true;
769                    }
770                } else {
771                    newMembers.add(members.get(i));
772                }
773            }
774        }
775
776        if (members.size() != newMembers.size())
777            throw new AssertionError();
778
779        members.clear();
780        members.addAll(newMembers);
781        fireTableDataChanged();
782        setSelectedMembers(sortedMembers);
783    }
784
785    /**
786     * Sort the selected relation members and all members below by the way they are linked.
787     */
788    public void sortBelow() {
789        final List<RelationMember> subList = members.subList(Math.max(0, getSelectionModel().getMinSelectionIndex()), members.size());
790        final List<RelationMember> sorted = relationSorter.sortMembers(subList);
791        subList.clear();
792        subList.addAll(sorted);
793        fireTableDataChanged();
794        setSelectedMembers(sorted);
795    }
796
797    WayConnectionType getWayConnection(int i) {
798        try {
799            if (connectionType == null) {
800                connectionType = wayConnectionTypeCalculator.updateLinks(members);
801            }
802            return connectionType.get(i);
803        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
804            throw BugReport.intercept(e).put("i", i).put("members", members).put("relation", relation);
805        }
806    }
807
808    @Override
809    public void tableChanged(TableModelEvent e) {
810        invalidateConnectionType();
811    }
812
813    private void invalidateConnectionType() {
814        connectionType = null;
815    }
816
817    /**
818     * Reverse the relation members.
819     */
820    @Override
821    public void reverse() {
822        List<Integer> selectedIndices = ArrayUtils.toList(getSelectedIndices());
823        List<Integer> selectedIndicesReversed = ArrayUtils.toList(getSelectedIndices());
824
825        if (selectedIndices.size() <= 1) {
826            Collections.reverse(members);
827            fireTableDataChanged();
828            setSelectedMembers(members);
829        } else {
830            Collections.reverse(selectedIndicesReversed);
831
832            List<RelationMember> newMembers = new ArrayList<>(members);
833
834            for (int i = 0; i < selectedIndices.size(); i++) {
835                newMembers.set(selectedIndices.get(i), members.get(selectedIndicesReversed.get(i)));
836            }
837
838            if (members.size() != newMembers.size()) throw new AssertionError();
839            members.clear();
840            members.addAll(newMembers);
841            fireTableDataChanged();
842            setSelectedMembersIdx(selectedIndices);
843        }
844    }
845}