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.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.List; 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 setDragEnabled(true); 074 setDropMode(DropMode.INSERT_ROWS); 075 } 076 } 077 078 @Override 079 protected ZoomToAction buildZoomToAction() { 080 return new ZoomToAction(this); 081 } 082 083 @Override 084 protected JPopupMenu buildPopupMenu() { 085 JPopupMenu menu = super.buildPopupMenu(); 086 zoomToGap = new ZoomToGapAction(); 087 registerListeners(); 088 getSelectionModel().addListSelectionListener(zoomToGap); 089 menu.add(zoomToGap); 090 menu.addSeparator(); 091 menu.add(new SelectPreviousGapAction()); 092 menu.add(new SelectNextGapAction()); 093 return menu; 094 } 095 096 @Override 097 public Dimension getPreferredSize() { 098 return getPreferredFullWidthSize(); 099 } 100 101 @Override 102 public void makeMemberVisible(int index) { 103 scrollRectToVisible(getCellRect(index, 0, true)); 104 } 105 106 private transient ListSelectionListener highlighterListener = new ListSelectionListener() { 107 @Override 108 public void valueChanged(ListSelectionEvent lse) { 109 if (Main.isDisplayingMapView()) { 110 Collection<RelationMember> sel = getMemberTableModel().getSelectedMembers(); 111 final List<OsmPrimitive> toHighlight = new ArrayList<>(); 112 for (RelationMember r: sel) { 113 if (r.getMember().isUsable()) { 114 toHighlight.add(r.getMember()); 115 } 116 } 117 SwingUtilities.invokeLater(new Runnable() { 118 @Override 119 public void run() { 120 if (Main.isDisplayingMapView() && highlightHelper.highlightOnly(toHighlight)) { 121 Main.map.mapView.repaint(); 122 } 123 } 124 }); 125 } 126 } 127 }; 128 129 private void initHighlighting() { 130 highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true); 131 if (!highlightEnabled) return; 132 getMemberTableModel().getSelectionModel().addListSelectionListener(highlighterListener); 133 if (Main.isDisplayingMapView()) { 134 HighlightHelper.clearAllHighlighted(); 135 Main.map.mapView.repaint(); 136 } 137 } 138 139 @Override 140 public void registerListeners() { 141 Main.getLayerManager().addLayerChangeListener(zoomToGap); 142 Main.getLayerManager().addActiveLayerChangeListener(zoomToGap); 143 super.registerListeners(); 144 } 145 146 @Override 147 public void unregisterListeners() { 148 super.unregisterListeners(); 149 Main.getLayerManager().removeLayerChangeListener(zoomToGap); 150 Main.getLayerManager().removeActiveLayerChangeListener(zoomToGap); 151 } 152 153 public void stopHighlighting() { 154 if (highlighterListener == null) return; 155 if (!highlightEnabled) return; 156 getMemberTableModel().getSelectionModel().removeListSelectionListener(highlighterListener); 157 highlighterListener = null; 158 if (Main.isDisplayingMapView()) { 159 HighlightHelper.clearAllHighlighted(); 160 Main.map.mapView.repaint(); 161 } 162 } 163 164 private class SelectPreviousGapAction extends AbstractAction { 165 166 SelectPreviousGapAction() { 167 putValue(NAME, tr("Select previous Gap")); 168 putValue(SHORT_DESCRIPTION, tr("Select the previous relation member which gives rise to a gap")); 169 } 170 171 @Override 172 public void actionPerformed(ActionEvent e) { 173 int i = getSelectedRow() - 1; 174 while (i >= 0 && getMemberTableModel().getWayConnection(i).linkPrev) { 175 i--; 176 } 177 if (i >= 0) { 178 getSelectionModel().setSelectionInterval(i, i); 179 } 180 } 181 } 182 183 private class SelectNextGapAction extends AbstractAction { 184 185 SelectNextGapAction() { 186 putValue(NAME, tr("Select next Gap")); 187 putValue(SHORT_DESCRIPTION, tr("Select the next 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 < getRowCount() && getMemberTableModel().getWayConnection(i).linkNext) { 194 i++; 195 } 196 if (i < getRowCount()) { 197 getSelectionModel().setSelectionInterval(i, i); 198 } 199 } 200 } 201 202 private class ZoomToGapAction extends AbstractAction implements LayerChangeListener, ActiveLayerChangeListener, ListSelectionListener { 203 204 /** 205 * Constructs a new {@code ZoomToGapAction}. 206 */ 207 ZoomToGapAction() { 208 putValue(NAME, tr("Zoom to Gap")); 209 putValue(SHORT_DESCRIPTION, tr("Zoom to the gap in the way sequence")); 210 updateEnabledState(); 211 } 212 213 private WayConnectionType getConnectionType() { 214 return getMemberTableModel().getWayConnection(getSelectedRows()[0]); 215 } 216 217 private final Collection<Direction> connectionTypesOfInterest = Arrays.asList( 218 WayConnectionType.Direction.FORWARD, WayConnectionType.Direction.BACKWARD); 219 220 private boolean hasGap() { 221 WayConnectionType connectionType = getConnectionType(); 222 return connectionTypesOfInterest.contains(connectionType.direction) 223 && !(connectionType.linkNext && connectionType.linkPrev); 224 } 225 226 @Override 227 public void actionPerformed(ActionEvent e) { 228 WayConnectionType connectionType = getConnectionType(); 229 Way way = (Way) getMemberTableModel().getReferredPrimitive(getSelectedRows()[0]); 230 if (!connectionType.linkPrev) { 231 getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction) 232 ? way.firstNode() : way.lastNode()); 233 AutoScaleAction.autoScale("selection"); 234 } else if (!connectionType.linkNext) { 235 getLayer().data.setSelected(WayConnectionType.Direction.FORWARD.equals(connectionType.direction) 236 ? way.lastNode() : way.firstNode()); 237 AutoScaleAction.autoScale("selection"); 238 } 239 } 240 241 private void updateEnabledState() { 242 setEnabled(Main.main != null 243 && Main.getLayerManager().getEditLayer() == getLayer() 244 && getSelectedRowCount() == 1 245 && hasGap()); 246 } 247 248 @Override 249 public void valueChanged(ListSelectionEvent e) { 250 updateEnabledState(); 251 } 252 253 @Override 254 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 255 updateEnabledState(); 256 } 257 258 @Override 259 public void layerAdded(LayerAddEvent e) { 260 updateEnabledState(); 261 } 262 263 @Override 264 public void layerRemoving(LayerRemoveEvent e) { 265 updateEnabledState(); 266 } 267 268 @Override 269 public void layerOrderChanged(LayerOrderChangeEvent e) { 270 // Do nothing 271 } 272 } 273 274 protected MemberTableModel getMemberTableModel() { 275 return (MemberTableModel) getModel(); 276 } 277}