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.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 = Arrays.asList( 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}