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}