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