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.Arrays;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.Set;
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            if (!GraphicsEnvironment.isHeadless()) {
074                setDragEnabled(true);
075            }
076            setDropMode(DropMode.INSERT_ROWS);
077        }
078    }
079
080    @Override
081    protected ZoomToAction buildZoomToAction() {
082        return new ZoomToAction(this);
083    }
084
085    @Override
086    protected JPopupMenu buildPopupMenu() {
087        JPopupMenu menu = super.buildPopupMenu();
088        zoomToGap = new ZoomToGapAction();
089        registerListeners();
090        menu.addSeparator();
091        getSelectionModel().addListSelectionListener(zoomToGap);
092        menu.add(zoomToGap);
093        menu.addSeparator();
094        menu.add(new SelectPreviousGapAction());
095        menu.add(new SelectNextGapAction());
096        return menu;
097    }
098
099    @Override
100    public Dimension getPreferredSize() {
101        return getPreferredFullWidthSize();
102    }
103
104    @Override
105    public void makeMemberVisible(int index) {
106        scrollRectToVisible(getCellRect(index, 0, true));
107    }
108
109    private transient ListSelectionListener highlighterListener = lse -> {
110        if (Main.isDisplayingMapView()) {
111            Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers();
112            final Set<OsmPrimitive> toHighlight = new HashSet<>();
113            for (RelationMember r: sel) {
114                if (r.getMember().isUsable()) {
115                    toHighlight.add(r.getMember());
116                }
117            }
118            SwingUtilities.invokeLater(() -> {
119                if (Main.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) {
120                    Main.map.mapView.repaint();
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 registerListeners() {
138        Main.getLayerManager().addLayerChangeListener(zoomToGap);
139        Main.getLayerManager().addActiveLayerChangeListener(zoomToGap);
140        super.registerListeners();
141    }
142
143    @Override
144    public void unregisterListeners() {
145        super.unregisterListeners();
146        Main.getLayerManager().removeLayerChangeListener(zoomToGap);
147        Main.getLayerManager().removeActiveLayerChangeListener(zoomToGap);
148    }
149
150    public void stopHighlighting() {
151        if (highlighterListener == null) return;
152        if (!highlightEnabled) return;
153        getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener);
154        highlighterListener = null;
155        if (Main.isDisplayingMapView()) {
156            HighlightHelper.clearAllHighlighted();
157            Main.map.mapView.repaint();
158        }
159    }
160
161    private class SelectPreviousGapAction extends AbstractAction {
162
163        SelectPreviousGapAction() {
164            putValue(NAME, tr("Select previous Gap"));
165            putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap"));
166        }
167
168        @Override
169        public void actionPerformed(ActionEvent e) {
170            int i = getSelectedRow() - 1;
171            while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) {
172                i--;
173            }
174            if (i >= 0) {
175                getSelectionModel().setSelectionInterval(i, i);
176            }
177        }
178    }
179
180    private class SelectNextGapAction extends AbstractAction {
181
182        SelectNextGapAction() {
183            putValue(NAME, tr("Select next Gap"));
184            putValue(SHORT_DESCRIPTION, tr("Select the next relation member which gives rise to a gap"));
185        }
186
187        @Override
188        public void actionPerformed(ActionEvent e) {
189            int i = getSelectedRow() + 1;
190            while (i < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) {
191                i++;
192            }
193            if (i < getRowCount()) {
194                getSelectionModel().setSelectionInterval(i, i);
195            }
196        }
197    }
198
199    private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ActiveLayerChangeListener, ListSelectionListener {
200
201        /**
202         * Constructs a new {@code ZoomToGapAction}.
203         */
204        ZoomToGapAction() {
205            putValue(NAME, tr("Zoom to Gap"));
206            putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence"));
207            updateEnabledState();
208        }
209
210        private WayConnectionType getConnectionType() {
211            return getMemberTableModel().getWayConnection(getSelectedRows()[0]);
212        }
213
214        private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(
215                WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD);
216
217        private boolean hasGap() {
218            WayConnectionType connectionType = getConnectionType();
219            return connectionTypesOfInterest.contains(connectionType.direction)
220                    && !(connectionType.linkNext && connectionType.linkPrev);
221        }
222
223        @Override
224        public void actionPerformed(ActionEvent e) {
225            WayConnectionType connectionType = getConnectionType();
226            Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]);
227            if (!connectionType.linkPrev) {
228                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
229                        ? way.firstNode() : way.lastNode());
230                AutoScaleAction.autoScale("selection");
231            } else if (!connectionType.linkNext) {
232                getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
233                        ? way.lastNode() : way.firstNode());
234                AutoScaleAction.autoScale("selection");
235            }
236        }
237
238        private void updateEnabledState() {
239            setEnabled(Main.main != null
240                    && Main.getLayerManager().getEditLayer() == getLayer()
241                    && getSelectedRowCount() == 1
242                    && hasGap());
243        }
244
245        @Override
246        public void valueChanged(ListSelectionEvent e) {
247            updateEnabledState();
248        }
249
250        @Override
251        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
252            updateEnabledState();
253        }
254
255        @Override
256        public void layerAdded(LayerAddEvent e) {
257            updateEnabledState();
258        }
259
260        @Override
261        public void layerRemoving(LayerRemoveEvent e) {
262            updateEnabledState();
263        }
264
265        @Override
266        public void layerOrderChanged(LayerOrderChangeEvent e) {
267            // Do nothing
268        }
269    }
270
271    protected MemberTableModel getMemberTableModel() {
272        return (MemberTableModel) getModel();
273    }
274}