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