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