001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.relation;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.GraphicsEnvironment;
008import java.awt.event.ActionEvent;
009import java.util.Arrays;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.Set;
013
014import javax.swing.AbstractAction;
015import javax.swing.DropMode;
016import javax.swing.JPopupMenu;
017import javax.swing.JTable;
018import javax.swing.ListSelectionModel;
019import javax.swing.SwingUtilities;
020import javax.swing.event.ListSelectionEvent;
021import javax.swing.event.ListSelectionListener;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.actions.AutoScaleAction;
025import org.openstreetmap.josm.actions.ZoomToAction;
026import org.openstreetmap.josm.data.osm.OsmPrimitive;
027import org.openstreetmap.josm.data.osm.Relation;
028import org.openstreetmap.josm.data.osm.RelationMember;
029import org.openstreetmap.josm.data.osm.Way;
030import org.openstreetmap.josm.gui.MainApplication;
031import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
032import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
033import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
034import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
035import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
036import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
037import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
038import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
039import org.openstreetmap.josm.gui.layer.OsmDataLayer;
040import org.openstreetmap.josm.gui.util.HighlightHelper;
041import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
042import org.openstreetmap.josm.spi.preferences.Config;
043
044/**
045 * The table of members a selected relation has.
046 */
047public class MemberTable extends OsmPrimitivesTable implements IMemberModelListener {
048
049    /** the additional actions in popup menu */
050    private ZoomToGapAction zoomToGap;
051    private final transient HighlightHelper highlightHelper = new HighlightHelper();
052    private boolean highlightEnabled;
053
054    /**
055     * constructor for relation member table
056     *
057     * @param layer the data layer of the relation. Must not be null
058     * @param relation the relation. Can be null
059     * @param model the table model
060     */
061    public MemberTable(OsmDataLayer layer, Relation relation, MemberTableModel model) {
062        super(model, new MemberTableColumnModel(layer.data, relation), model.getSelectionModel());
063        setLayer(layer);
064        model.addMemberModelListener(this);
065
066        MemberRoleCellEditor ce = (MemberRoleCellEditor) getColumnModel().getColumn(0).getCellEditor();
067        setRowHeight(ce.getEditor().getPreferredSize().height);
068        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
069        setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
070        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
071
072        installCustomNavigation(0);
073        initHighlighting();
074
075        if (!GraphicsEnvironment.isHeadless()) {
076            setTransferHandler(new MemberTransferHandler());
077            setFillsViewportHeight(true); // allow drop on empty table
078            if (!GraphicsEnvironment.isHeadless()) {
079                setDragEnabled(true);
080            }
081            setDropMode(DropMode.INSERT_ROWS);
082        }
083    }
084
085    @Override
086    protected ZoomToAction buildZoomToAction() {
087        return new ZoomToAction(this);
088    }
089
090    @Override
091    protected JPopupMenu buildPopupMenu() {
092        JPopupMenu menu = super.buildPopupMenu();
093        zoomToGap = new ZoomToGapAction();
094        registerListeners();
095        menu.addSeparator();
096        getSelectionModel().addListSelectionListener(zoomToGap);
097        menu.add(zoomToGap);
098        menu.addSeparator();
099        menu.add(new SelectPreviousGapAction());
100        menu.add(new SelectNextGapAction());
101        return menu;
102    }
103
104    @Override
105    public Dimension getPreferredSize() {
106        return getPreferredFullWidthSize();
107    }
108
109    @Override
110    public void makeMemberVisible(int index) {
111        scrollRectToVisible(getCellRect(index, 0, true));
112    }
113
114    private transient ListSelectionListener highlighterListener = lse -> {
115        if (MainApplication.isDisplayingMapView()) {
116            Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers();
117            final Set<OsmPrimitive> toHighlight = new HashSet<>();
118            for (RelationMember r: sel) {
119                if (r.getMember().isUsable()) {
120                    toHighlight.add(r.getMember());
121                }
122            }
123            SwingUtilities.invokeLater(() -> {
124                if (MainApplication.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) {
125                    MainApplication.getMap().mapView.repaint();
126                }
127            });
128        }
129    };
130
131    private void initHighlighting() {
132        highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);
133        if (!highlightEnabled) return;
134        getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener);
135        if (MainApplication.isDisplayingMapView()) {
136            HighlightHelper.clearAllHighlighted();
137            MainApplication.getMap().mapView.repaint();
138        }
139    }
140
141    @Override
142    public void registerListeners() {
143        MainApplication.getLayerManager().addLayerChangeListener(zoomToGap);
144        MainApplication.getLayerManager().addActiveLayerChangeListener(zoomToGap);
145        super.registerListeners();
146    }
147
148    @Override
149    public void unregisterListeners() {
150        super.unregisterListeners();
151        MainApplication.getLayerManager().removeLayerChangeListener(zoomToGap);
152        MainApplication.getLayerManager().removeActiveLayerChangeListener(zoomToGap);
153    }
154
155    public void stopHighlighting() {
156        if (highlighterListener == null) return;
157        if (!highlightEnabled) return;
158        getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener);
159        highlighterListener = null;
160        if (MainApplication.isDisplayingMapView()) {
161            HighlightHelper.clearAllHighlighted();
162            MainApplication.getMap().mapView.repaint();
163        }
164    }
165
166    private class SelectPreviousGapAction extends AbstractAction {
167
168        SelectPreviousGapAction() {
169            putValue(NAME, tr("Select previous Gap"));
170            putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap"));
171        }
172
173        @Override
174        public void actionPerformed(ActionEvent e) {
175            int i = getSelectedRow() - 1;
176            while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) {
177                i--;
178            }
179            if (i >= 0) {
180                getSelectionModel().setSelectionInterval(i, i);
181                getMemberTableModel().fireMakeMemberVisible(i);
182            }
183        }
184    }
185
186    private class SelectNextGapAction extends AbstractAction {
187
188        SelectNextGapAction() {
189            putValue(NAME, tr("Select next Gap"));
190            putValue(SHORT_DESCRIPTION, tr("Select the next relation member which gives rise to a gap"));
191        }
192
193        @Override
194        public void actionPerformed(ActionEvent e) {
195            int i = getSelectedRow() + 1;
196            while (i < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) {
197                i++;
198            }
199            if (i < getRowCount()) {
200                getSelectionModel().setSelectionInterval(i, i);
201                getMemberTableModel().fireMakeMemberVisible(i);
202            }
203        }
204    }
205
206    private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ActiveLayerChangeListener, ListSelectionListener {
207
208        /**
209         * Constructs a new {@code ZoomToGapAction}.
210         */
211        ZoomToGapAction() {
212            putValue(NAME, tr("Zoom to Gap"));
213            putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence"));
214            updateEnabledState();
215        }
216
217        private WayConnectionType getConnectionType() {
218            return getMemberTableModel().getWayConnection(getSelectedRows()[0]);
219        }
220
221        private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(
222                WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD);
223
224        private boolean hasGap() {
225            WayConnectionType connectionType = getConnectionType();
226            return connectionTypesOfInterest.contains(connectionType.direction)
227                    && !(connectionType.linkNext && connectionType.linkPrev);
228        }
229
230        @Override
231        public void actionPerformed(ActionEvent e) {
232            WayConnectionType connectionType = getConnectionType();
233            Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]);
234            if (!connectionType.linkPrev) {
235                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
236                        ? way.firstNode() : way.lastNode());
237                AutoScaleAction.autoScale("selection");
238            } else if (!connectionType.linkNext) {
239                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
240                        ? way.lastNode() : way.firstNode());
241                AutoScaleAction.autoScale("selection");
242            }
243        }
244
245        private void updateEnabledState() {
246            setEnabled(Main.main != null
247                    && MainApplication.getLayerManager().getEditLayer() == getLayer()
248                    && getSelectedRowCount() == 1
249                    && hasGap());
250        }
251
252        @Override
253        public void valueChanged(ListSelectionEvent e) {
254            updateEnabledState();
255        }
256
257        @Override
258        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
259            updateEnabledState();
260        }
261
262        @Override
263        public void layerAdded(LayerAddEvent e) {
264            updateEnabledState();
265        }
266
267        @Override
268        public void layerRemoving(LayerRemoveEvent e) {
269            updateEnabledState();
270        }
271
272        @Override
273        public void layerOrderChanged(LayerOrderChangeEvent e) {
274            // Do nothing
275        }
276    }
277
278    protected MemberTableModel getMemberTableModel() {
279        return (MemberTableModel) getModel();
280    }
281}