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