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.GraphicsEnvironment;
009import java.awt.event.ActionEvent;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.List;
014
015import javax.swing.AbstractAction;
016import javax.swing.DropMode;
017import javax.swing.JPopupMenu;
018import javax.swing.JTable;
019import javax.swing.JViewport;
020import javax.swing.ListSelectionModel;
021import javax.swing.SwingUtilities;
022import javax.swing.event.ListSelectionEvent;
023import javax.swing.event.ListSelectionListener;
024
025import org.openstreetmap.josm.Main;
026import org.openstreetmap.josm.actions.AutoScaleAction;
027import org.openstreetmap.josm.actions.ZoomToAction;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.data.osm.Relation;
030import org.openstreetmap.josm.data.osm.RelationMember;
031import org.openstreetmap.josm.data.osm.Way;
032import org.openstreetmap.josm.gui.MapView;
033import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
034import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
035import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
036import org.openstreetmap.josm.gui.layer.Layer;
037import org.openstreetmap.josm.gui.layer.OsmDataLayer;
038import org.openstreetmap.josm.gui.util.HighlightHelper;
039import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
040
041public class MemberTable extends OsmPrimitivesTable implements IMemberModelListener {
042
043    /** the additional actions in popup menu */
044    private ZoomToGapAction zoomToGap;
045    private final transient HighlightHelper highlightHelper = new HighlightHelper();
046    private boolean highlightEnabled;
047
048    /**
049     * constructor for relation member table
050     *
051     * @param layer the data layer of the relation. Must not be null
052     * @param relation the relation. Can be null
053     * @param model the table model
054     */
055    public MemberTable(OsmDataLayer layer, Relation relation, MemberTableModel model) {
056        super(model, new MemberTableColumnModel(layer.data, relation), model.getSelectionModel());
057        setLayer(layer);
058        model.addMemberModelListener(this);
059
060        MemberRoleCellEditor ce = (MemberRoleCellEditor) getColumnModel().getColumn(0).getCellEditor();
061        setRowHeight(ce.getEditor().getPreferredSize().height);
062        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
063        setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
064        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
065
066        installCustomNavigation(0);
067        initHighlighting();
068
069        if (!GraphicsEnvironment.isHeadless()) {
070            setTransferHandler(new MemberTransferHandler());
071            setFillsViewportHeight(true); // allow drop on empty table
072            setDragEnabled(true);
073            setDropMode(DropMode.INSERT_ROWS);
074        }
075    }
076
077    @Override
078    protected ZoomToAction buildZoomToAction() {
079        return new ZoomToAction(this);
080    }
081
082    @Override
083    protected JPopupMenu buildPopupMenu() {
084        JPopupMenu menu = super.buildPopupMenu();
085        zoomToGap = new ZoomToGapAction();
086        MapView.addLayerChangeListener(zoomToGap);
087        getSelectionModel().addListSelectionListener(zoomToGap);
088        menu.add(zoomToGap);
089        menu.addSeparator();
090        menu.add(new SelectPreviousGapAction());
091        menu.add(new SelectNextGapAction());
092        return menu;
093    }
094
095    @Override
096    public Dimension getPreferredSize() {
097        Container c = getParent();
098        while (c != null && !(c instanceof JViewport)) {
099            c = c.getParent();
100        }
101        if (c != null) {
102            Dimension d = super.getPreferredSize();
103            d.width = c.getSize().width;
104            return d;
105        }
106        return super.getPreferredSize();
107    }
108
109    @Override
110    public void makeMemberVisible(int index) {
111        scrollRectToVisible(getCellRect(index, 0, true));
112    }
113
114    private transient ListSelectionListener highlighterListener = new ListSelectionListener() {
115        @Override
116        public void valueChanged(ListSelectionEvent lse) {
117            if (Main.isDisplayingMapView()) {
118                Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers();
119                final List<OsmPrimitive> toHighlight = new ArrayList<>();
120                for (RelationMember r: sel) {
121                    if (r.getMember().isUsable()) {
122                        toHighlight.add(r.getMember());
123                    }
124                }
125                SwingUtilities.invokeLater(new Runnable() {
126                    @Override
127                    public void run() {
128                        if (Main.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) {
129                            Main.map.mapView.repaint();
130                        }
131                    }
132                });
133            }
134        }
135    };
136
137    private void initHighlighting() {
138        highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
139        if (!highlightEnabled) return;
140        getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener);
141        if (Main.isDisplayingMapView()) {
142            HighlightHelper.clearAllHighlighted();
143            Main.map.mapView.repaint();
144        }
145    }
146
147    @Override
148    public void unlinkAsListener() {
149        super.unlinkAsListener();
150        MapView.removeLayerChangeListener(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, 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.main.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 activeLayerChange(Layer oldLayer, Layer newLayer) {
255            updateEnabledState();
256        }
257
258        @Override
259        public void layerAdded(Layer newLayer) {
260            updateEnabledState();
261        }
262
263        @Override
264        public void layerRemoved(Layer oldLayer) {
265            updateEnabledState();
266        }
267    }
268
269    protected MemberTableModel getMemberTableModel() {
270        return (MemberTableModel) getModel();
271    }
272}