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