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