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