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.Container;
007import java.awt.Dimension;
008import java.awt.KeyboardFocusManager;
009import java.awt.event.ActionEvent;
010import java.awt.event.KeyEvent;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.List;
015
016import javax.swing.AbstractAction;
017import javax.swing.JComponent;
018import javax.swing.JPopupMenu;
019import javax.swing.JTable;
020import javax.swing.JViewport;
021import javax.swing.KeyStroke;
022import javax.swing.ListSelectionModel;
023import javax.swing.SwingUtilities;
024import javax.swing.event.ListSelectionEvent;
025import javax.swing.event.ListSelectionListener;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.actions.AutoScaleAction;
029import org.openstreetmap.josm.actions.ZoomToAction;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.data.osm.Relation;
032import org.openstreetmap.josm.data.osm.RelationMember;
033import org.openstreetmap.josm.data.osm.Way;
034import org.openstreetmap.josm.gui.MapView;
035import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
036import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
037import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
038import org.openstreetmap.josm.gui.layer.Layer;
039import org.openstreetmap.josm.gui.layer.OsmDataLayer;
040import org.openstreetmap.josm.gui.util.HighlightHelper;
041import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
042
043public class MemberTable extends OsmPrimitivesTable implements IMemberModelListener {
044
045    /** the additional actions in popup menu */
046    private ZoomToGapAction zoomToGap;
047    private HighlightHelper highlightHelper = new HighlightHelper();
048    private boolean highlightEnabled;
049
050    /**
051     * constructor for relation member table
052     *
053     * @param layer the data layer of the relation. Must not be null
054     * @param relation the relation. Can be null
055     * @param model the table model
056     */
057    public MemberTable(OsmDataLayer layer, Relation relation, MemberTableModel model) {
058        super(model, new MemberTableColumnModel(layer.data, relation), model.getSelectionModel());
059        setLayer(layer);
060        model.addMemberModelListener(this);
061        init();
062    }
063
064    /**
065     * initialize the table
066     */
067    protected void init() {
068        MemberRoleCellEditor ce = (MemberRoleCellEditor)getColumnModel().getColumn(0).getCellEditor();
069        setRowHeight(ce.getEditor().getPreferredSize().height);
070        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
071        setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
072        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
073
074        // make ENTER behave like TAB
075        //
076        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
077                KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
078
079        initHighlighting();
080
081        // install custom navigation actions
082        //
083        getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
084        getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
085    }
086
087    @Override
088    protected ZoomToAction buildZoomToAction() {
089        return new ZoomToAction(this);
090    }
091
092    @Override
093    protected JPopupMenu buildPopupMenu() {
094        JPopupMenu menu = super.buildPopupMenu();
095        zoomToGap = new ZoomToGapAction();
096        MapView.addLayerChangeListener(zoomToGap);
097        getSelectionModel().addListSelectionListener(zoomToGap);
098        menu.add(zoomToGap);
099        menu.addSeparator();
100        menu.add(new SelectPreviousGapAction());
101        menu.add(new SelectNextGapAction());
102        return menu;
103    }
104
105    @Override
106    public Dimension getPreferredSize(){
107        Container c = getParent();
108        while(c != null && ! (c instanceof JViewport)) {
109            c = c.getParent();
110        }
111        if (c != null) {
112            Dimension d = super.getPreferredSize();
113            d.width = c.getSize().width;
114            return d;
115        }
116        return super.getPreferredSize();
117    }
118
119    @Override
120    public void makeMemberVisible(int index) {
121        scrollRectToVisible(getCellRect(index, 0, true));
122    }
123
124    ListSelectionListener highlighterListener = new ListSelectionListener() {
125            @Override
126            public void valueChanged(ListSelectionEvent lse) {
127                if (Main.isDisplayingMapView()) {
128                    Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers();
129                    final List<OsmPrimitive> toHighlight = new ArrayList<>();
130                    for (RelationMember r: sel) {
131                        if (r.getMember().isUsable()) {
132                            toHighlight.add(r.getMember());
133                        }
134                    }
135                    SwingUtilities.invokeLater(new Runnable() {
136                        @Override
137                        public void run() {
138                            if (highlightHelper.highlightOnly(toHighlight)) {
139                                Main.map.mapView.repaint();
140                            }
141                        }
142                    });
143                }
144            }};
145
146    private void initHighlighting() {
147        highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
148        if (!highlightEnabled) return;
149        getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener);
150        if (Main.isDisplayingMapView()) {
151            HighlightHelper.clearAllHighlighted();
152            Main.map.mapView.repaint();
153        }
154    }
155
156    /**
157     * Action to be run when the user navigates to the next cell in the table, for instance by
158     * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul>
159     * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row
160     * when the user leaves the last cell in the table</li></ul>
161     */
162    class SelectNextColumnCellAction extends AbstractAction {
163        @Override
164        public void actionPerformed(ActionEvent e) {
165            run();
166        }
167
168        public void run() {
169            int col = getSelectedColumn();
170            int row = getSelectedRow();
171            if (getCellEditor() != null) {
172                getCellEditor().stopCellEditing();
173            }
174
175            if (col == 0 && row < getRowCount() - 1) {
176                row++;
177            } else if (row < getRowCount() - 1) {
178                col = 0;
179                row++;
180            } else {
181                // go to next component, no more rows in this table
182                KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
183                manager.focusNextComponent();
184                return;
185            }
186            changeSelection(row, col, false, false);
187        }
188    }
189
190    /**
191     * Action to be run when the user navigates to the previous cell in the table, for instance by
192     * pressing Shift-TAB
193     */
194    private class SelectPreviousColumnCellAction extends AbstractAction {
195
196        @Override
197        public void actionPerformed(ActionEvent e) {
198            int col = getSelectedColumn();
199            int row = getSelectedRow();
200            if (getCellEditor() != null) {
201                getCellEditor().stopCellEditing();
202            }
203
204            if (col <= 0 && row <= 0) {
205                // change nothing
206            } else if (row > 0) {
207                col = 0;
208                row--;
209            }
210            changeSelection(row, col, false, false);
211        }
212    }
213
214    @Override
215    public void unlinkAsListener() {
216        super.unlinkAsListener();
217        MapView.removeLayerChangeListener(zoomToGap);
218    }
219
220    public void stopHighlighting() {
221        if (highlighterListener == null) return;
222        if (!highlightEnabled) return;
223        getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener);
224        highlighterListener = null;
225        if (Main.isDisplayingMapView()) {
226            HighlightHelper.clearAllHighlighted();
227            Main.map.mapView.repaint();
228        }
229    }
230
231    private class SelectPreviousGapAction extends AbstractAction {
232
233        public SelectPreviousGapAction() {
234            putValue(NAME, tr("Select previous Gap"));
235            putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap"));
236        }
237
238        @Override
239        public void actionPerformed(ActionEvent e) {
240            int i = getSelectedRow() - 1;
241            while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) {
242                i--;
243            }
244            if (i >= 0) {
245                getSelectionModel().setSelectionInterval(i, i);
246            }
247        }
248    }
249
250    private class SelectNextGapAction extends AbstractAction {
251
252        public SelectNextGapAction() {
253            putValue(NAME, tr("Select next Gap"));
254            putValue(SHORT_DESCRIPTION, tr("Select the next relation member which gives rise to a gap"));
255        }
256
257        @Override
258        public void actionPerformed(ActionEvent e) {
259            int i = getSelectedRow() + 1;
260            while (i < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) {
261                i++;
262            }
263            if (i < getRowCount()) {
264                getSelectionModel().setSelectionInterval(i, i);
265            }
266        }
267    }
268
269    private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ListSelectionListener {
270
271        public ZoomToGapAction() {
272            putValue(NAME, tr("Zoom to Gap"));
273            putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence"));
274            updateEnabledState();
275        }
276
277        private WayConnectionType getConnectionType() {
278            return getMemberTableModel().getWayConnection(getSelectedRows()[0]);
279        }
280
281        private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD);
282
283        private boolean hasGap() {
284            WayConnectionType connectionType = getConnectionType();
285            return connectionTypesOfInterest.contains(connectionType.direction)
286                    && !(connectionType.linkNext && connectionType.linkPrev);
287        }
288
289        @Override
290        public void actionPerformed(ActionEvent e) {
291            WayConnectionType connectionType = getConnectionType();
292            Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]);
293            if (!connectionType.linkPrev) {
294                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
295                        ? way.firstNode() : way.lastNode());
296                AutoScaleAction.autoScale("selection");
297            } else if (!connectionType.linkNext) {
298                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
299                        ? way.lastNode() : way.firstNode());
300                AutoScaleAction.autoScale("selection");
301            }
302        }
303
304        private void updateEnabledState() {
305            setEnabled(Main.main != null
306                    && Main.main.getEditLayer() == getLayer()
307                    && getSelectedRowCount() == 1
308                    && hasGap());
309        }
310
311        @Override
312        public void valueChanged(ListSelectionEvent e) {
313            updateEnabledState();
314        }
315
316        @Override
317        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
318            updateEnabledState();
319        }
320
321        @Override
322        public void layerAdded(Layer newLayer) {
323            updateEnabledState();
324        }
325
326        @Override
327        public void layerRemoved(Layer oldLayer) {
328            updateEnabledState();
329        }
330    }
331
332    protected MemberTableModel getMemberTableModel() {
333        return (MemberTableModel) getModel();
334    }
335}