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