// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.relation;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.DropMode;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.AutoScaleAction;
import org.openstreetmap.josm.actions.ZoomToAction;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.util.HighlightHelper;
import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
public class MemberTable extends OsmPrimitivesTable implements IMemberModelListener {
/** the additional actions in popup menu */
private ZoomToGapAction zoomToGap;
private final transient HighlightHelper highlightHelper = new HighlightHelper();
private boolean highlightEnabled;
/**
* constructor for relation member table
*
* @param layer the data layer of the relation. Must not be null
* @param relation the relation. Can be null
* @param model the table model
*/
public MemberTable(OsmDataLayer layer, Relation relation, MemberTableModel model) {
super(model, new MemberTableColumnModel(layer.data, relation), model.getSelectionModel());
setLayer(layer);
model.addMemberModelListener(this);
MemberRoleCellEditor ce = (MemberRoleCellEditor) getColumnModel().getColumn(0).getCellEditor();
setRowHeight(ce.getEditor().getPreferredSize().height);
setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
installCustomNavigation(0);
initHighlighting();
if (!GraphicsEnvironment.isHeadless()) {
setTransferHandler(new MemberTransferHandler());
setFillsViewportHeight(true); // allow drop on empty table
if (!GraphicsEnvironment.isHeadless()) {
setDragEnabled(true);
}
setDropMode(DropMode.INSERT_ROWS);
}
}
@Override
protected ZoomToAction buildZoomToAction() {
return new ZoomToAction(this);
}
@Override
protected JPopupMenu buildPopupMenu() {
JPopupMenu menu = super.buildPopupMenu();
zoomToGap = new ZoomToGapAction();
registerListeners();
menu.addSeparator();
getSelectionModel().addListSelectionListener(zoomToGap);
menu.add(zoomToGap);
menu.addSeparator();
menu.add(new SelectPreviousGapAction());
menu.add(new SelectNextGapAction());
return menu;
}
@Override
public Dimension getPreferredSize() {
return getPreferredFullWidthSize();
}
@Override
public void makeMemberVisible(int index) {
scrollRectToVisible(getCellRect(index, 0, true));
}
private transient ListSelectionListener highlighterListener = lse -> {
if (Main.isDisplayingMapView()) {
Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers();
final Set<OsmPrimitive> toHighlight = new HashSet<>();
for (RelationMember r: sel) {
if (r.getMember().isUsable()) {
toHighlight.add(r.getMember());
}
}
SwingUtilities.invokeLater(() -> {
if (Main.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) {
Main.map.mapView.repaint();
}
});
}
};
private void initHighlighting() {
highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
if (!highlightEnabled) return;
getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener);
if (Main.isDisplayingMapView()) {
HighlightHelper.clearAllHighlighted();
Main.map.mapView.repaint();
}
}
@Override
public void registerListeners() {
Main.getLayerManager().addLayerChangeListener(zoomToGap);
Main.getLayerManager().addActiveLayerChangeListener(zoomToGap);
super.registerListeners();
}
@Override
public void unregisterListeners() {
super.unregisterListeners();
Main.getLayerManager().removeLayerChangeListener(zoomToGap);
Main.getLayerManager().removeActiveLayerChangeListener(zoomToGap);
}
public void stopHighlighting() {
if (highlighterListener == null) return;
if (!highlightEnabled) return;
getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener);
highlighterListener = null;
if (Main.isDisplayingMapView()) {
HighlightHelper.clearAllHighlighted();
Main.map.mapView.repaint();
}
}
private class SelectPreviousGapAction extends AbstractAction {
SelectPreviousGapAction() {
putValue(NAME, tr("Select previous Gap"));
putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap"));
}
@Override
public void actionPerformed(ActionEvent e) {
int i = getSelectedRow() - 1;
while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) {
i--;
}
if (i >= 0) {
getSelectionModel().setSelectionInterval(i, i);
}
}
}
private class SelectNextGapAction extends AbstractAction {
SelectNextGapAction() {
putValue(NAME, tr("Select next Gap"));
putValue(SHORT_DESCRIPTION, tr("Select the next relation member which gives rise to a gap"));
}
@Override
public void actionPerformed(ActionEvent e) {
int i = getSelectedRow() + 1;
while (i < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) {
i++;
}
if (i < getRowCount()) {
getSelectionModel().setSelectionInterval(i, i);
}
}
}
private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ActiveLayerChangeListener, ListSelectionListener {
/**
* Constructs a new {@code ZoomToGapAction}.
*/
ZoomToGapAction() {
putValue(NAME, tr("Zoom to Gap"));
putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence"));
updateEnabledState();
}
private WayConnectionType getConnectionType() {
return getMemberTableModel().getWayConnection(getSelectedRows()[0]);
}
private final Collection<Direction> connectionTypesOfInterest = Arrays.asList(
WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD);
private boolean hasGap() {
WayConnectionType connectionType = getConnectionType();
return connectionTypesOfInterest.contains(connectionType.direction)
&& !(connectionType.linkNext && connectionType.linkPrev);
}
@Override
public void actionPerformed(ActionEvent e) {
WayConnectionType connectionType = getConnectionType();
Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]);
if (!connectionType.linkPrev) {
getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
? way.firstNode() : way.lastNode());
AutoScaleAction.autoScale("selection");
} else if (!connectionType.linkNext) {
getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction)
? way.lastNode() : way.firstNode());
AutoScaleAction.autoScale("selection");
}
}
private void updateEnabledState() {
setEnabled(Main.main != null
&& Main.getLayerManager().getEditLayer() == getLayer()
&& getSelectedRowCount() == 1
&& hasGap());
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
updateEnabledState();
}
@Override
public void layerAdded(LayerAddEvent e) {
updateEnabledState();
}
@Override
public void layerRemoving(LayerRemoveEvent e) {
updateEnabledState();
}
@Override
public void layerOrderChanged(LayerOrderChangeEvent e) {
// Do nothing
}
}
protected MemberTableModel getMemberTableModel() {
return (MemberTableModel) getModel();
}
}