001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.history; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.Insets; 009import java.awt.Point; 010import java.awt.event.ActionEvent; 011import java.awt.event.MouseAdapter; 012import java.awt.event.MouseEvent; 013 014import javax.swing.AbstractAction; 015import javax.swing.JPanel; 016import javax.swing.JPopupMenu; 017import javax.swing.JScrollPane; 018import javax.swing.JTable; 019import javax.swing.ListSelectionModel; 020import javax.swing.event.TableModelEvent; 021import javax.swing.event.TableModelListener; 022import javax.swing.table.TableModel; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.actions.AutoScaleAction; 026import org.openstreetmap.josm.data.osm.OsmPrimitive; 027import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 028import org.openstreetmap.josm.data.osm.PrimitiveId; 029import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 030import org.openstreetmap.josm.data.osm.history.History; 031import org.openstreetmap.josm.data.osm.history.HistoryDataSet; 032import org.openstreetmap.josm.gui.layer.OsmDataLayer; 033import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer; 034import org.openstreetmap.josm.gui.util.GuiHelper; 035import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 036import org.openstreetmap.josm.tools.ImageProvider; 037 038/** 039 * NodeListViewer is a UI component which displays the node list of two 040 * version of a {@link OsmPrimitive} in a {@link History}. 041 * 042 * <ul> 043 * <li>on the left, it displays the node list for the version at {@link PointInTimeType#REFERENCE_POINT_IN_TIME}</li> 044 * <li>on the right, it displays the node list for the version at {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li> 045 * </ul> 046 * @since 1709 047 */ 048public class NodeListViewer extends JPanel { 049 050 private transient HistoryBrowserModel model; 051 private VersionInfoPanel referenceInfoPanel; 052 private VersionInfoPanel currentInfoPanel; 053 private transient AdjustmentSynchronizer adjustmentSynchronizer; 054 private transient SelectionSynchronizer selectionSynchronizer; 055 private NodeListPopupMenu popupMenu; 056 057 /** 058 * Constructs a new {@code NodeListViewer}. 059 * @param model history browser model 060 */ 061 public NodeListViewer(HistoryBrowserModel model) { 062 setModel(model); 063 build(); 064 } 065 066 protected JScrollPane embeddInScrollPane(JTable table) { 067 JScrollPane pane = new JScrollPane(table); 068 adjustmentSynchronizer.participateInSynchronizedScrolling(pane.getVerticalScrollBar()); 069 return pane; 070 } 071 072 protected JTable buildReferenceNodeListTable() { 073 final DiffTableModel tableModel = model.getNodeListTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME); 074 final NodeListTableColumnModel columnModel = new NodeListTableColumnModel(); 075 final JTable table = new JTable(tableModel, columnModel); 076 tableModel.addTableModelListener(new ReversedChangeListener(table, columnModel)); 077 table.setName("table.referencenodelisttable"); 078 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 079 selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel()); 080 table.addMouseListener(new InternalPopupMenuLauncher()); 081 table.addMouseListener(new DoubleClickAdapter(table)); 082 return table; 083 } 084 085 protected JTable buildCurrentNodeListTable() { 086 final DiffTableModel tableModel = model.getNodeListTableModel(PointInTimeType.CURRENT_POINT_IN_TIME); 087 final NodeListTableColumnModel columnModel = new NodeListTableColumnModel(); 088 final JTable table = new JTable(tableModel, columnModel); 089 tableModel.addTableModelListener(new ReversedChangeListener(table, columnModel)); 090 table.setName("table.currentnodelisttable"); 091 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 092 selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel()); 093 table.addMouseListener(new InternalPopupMenuLauncher()); 094 table.addMouseListener(new DoubleClickAdapter(table)); 095 return table; 096 } 097 098 protected void build() { 099 setLayout(new GridBagLayout()); 100 GridBagConstraints gc = new GridBagConstraints(); 101 102 // --------------------------- 103 gc.gridx = 0; 104 gc.gridy = 0; 105 gc.gridwidth = 1; 106 gc.gridheight = 1; 107 gc.weightx = 0.5; 108 gc.weighty = 0.0; 109 gc.insets = new Insets(5, 5, 5, 0); 110 gc.fill = GridBagConstraints.HORIZONTAL; 111 gc.anchor = GridBagConstraints.FIRST_LINE_START; 112 referenceInfoPanel = new VersionInfoPanel(model, PointInTimeType.REFERENCE_POINT_IN_TIME); 113 add(referenceInfoPanel, gc); 114 115 gc.gridx = 1; 116 gc.gridy = 0; 117 gc.gridwidth = 1; 118 gc.gridheight = 1; 119 gc.fill = GridBagConstraints.HORIZONTAL; 120 gc.weightx = 0.5; 121 gc.weighty = 0.0; 122 gc.anchor = GridBagConstraints.FIRST_LINE_START; 123 currentInfoPanel = new VersionInfoPanel(model, PointInTimeType.CURRENT_POINT_IN_TIME); 124 add(currentInfoPanel, gc); 125 126 adjustmentSynchronizer = new AdjustmentSynchronizer(); 127 selectionSynchronizer = new SelectionSynchronizer(); 128 129 popupMenu = new NodeListPopupMenu(); 130 131 // --------------------------- 132 gc.gridx = 0; 133 gc.gridy = 1; 134 gc.gridwidth = 1; 135 gc.gridheight = 1; 136 gc.weightx = 0.5; 137 gc.weighty = 1.0; 138 gc.fill = GridBagConstraints.BOTH; 139 gc.anchor = GridBagConstraints.NORTHWEST; 140 add(embeddInScrollPane(buildReferenceNodeListTable()), gc); 141 142 gc.gridx = 1; 143 gc.gridy = 1; 144 gc.gridwidth = 1; 145 gc.gridheight = 1; 146 gc.weightx = 0.5; 147 gc.weighty = 1.0; 148 gc.fill = GridBagConstraints.BOTH; 149 gc.anchor = GridBagConstraints.NORTHWEST; 150 add(embeddInScrollPane(buildCurrentNodeListTable()), gc); 151 } 152 153 protected void unregisterAsChangeListener(HistoryBrowserModel model) { 154 if (currentInfoPanel != null) { 155 model.removeChangeListener(currentInfoPanel); 156 } 157 if (referenceInfoPanel != null) { 158 model.removeChangeListener(referenceInfoPanel); 159 } 160 } 161 162 protected void registerAsChangeListener(HistoryBrowserModel model) { 163 if (currentInfoPanel != null) { 164 model.addChangeListener(currentInfoPanel); 165 } 166 if (referenceInfoPanel != null) { 167 model.addChangeListener(referenceInfoPanel); 168 } 169 } 170 171 /** 172 * Sets the history browser model. 173 * @param model the history browser model 174 */ 175 public void setModel(HistoryBrowserModel model) { 176 if (this.model != null) { 177 unregisterAsChangeListener(model); 178 } 179 this.model = model; 180 if (this.model != null) { 181 registerAsChangeListener(model); 182 } 183 } 184 185 static final class ReversedChangeListener implements TableModelListener { 186 private final NodeListTableColumnModel columnModel; 187 private final JTable table; 188 private Boolean reversed; 189 private final String nonReversedText; 190 private final String reversedText; 191 192 ReversedChangeListener(JTable table, NodeListTableColumnModel columnModel) { 193 this.columnModel = columnModel; 194 this.table = table; 195 nonReversedText = tr("Nodes") + (table.getFont().canDisplay('\u25bc') ? " \u25bc" : " (1-n)"); 196 reversedText = tr("Nodes") + (table.getFont().canDisplay('\u25b2') ? " \u25b2" : " (n-1)"); 197 } 198 199 @Override 200 public void tableChanged(TableModelEvent e) { 201 if (e.getSource() instanceof DiffTableModel) { 202 final DiffTableModel mod = (DiffTableModel) e.getSource(); 203 if (reversed == null || reversed != mod.isReversed()) { 204 reversed = mod.isReversed(); 205 columnModel.getColumn(0).setHeaderValue(reversed ? reversedText : nonReversedText); 206 table.getTableHeader().setToolTipText( 207 reversed ? tr("The nodes of this way are in reverse order") : null); 208 table.getTableHeader().repaint(); 209 } 210 } 211 } 212 } 213 214 static class NodeListPopupMenu extends JPopupMenu { 215 private final ZoomToNodeAction zoomToNodeAction; 216 private final ShowHistoryAction showHistoryAction; 217 218 NodeListPopupMenu() { 219 zoomToNodeAction = new ZoomToNodeAction(); 220 add(zoomToNodeAction); 221 showHistoryAction = new ShowHistoryAction(); 222 add(showHistoryAction); 223 } 224 225 public void prepare(PrimitiveId pid) { 226 zoomToNodeAction.setPrimitiveId(pid); 227 zoomToNodeAction.updateEnabledState(); 228 229 showHistoryAction.setPrimitiveId(pid); 230 showHistoryAction.updateEnabledState(); 231 } 232 } 233 234 static class ZoomToNodeAction extends AbstractAction { 235 private transient PrimitiveId primitiveId; 236 237 /** 238 * Constructs a new {@code ZoomToNodeAction}. 239 */ 240 ZoomToNodeAction() { 241 putValue(NAME, tr("Zoom to node")); 242 putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer")); 243 putValue(SMALL_ICON, ImageProvider.get("dialogs", "zoomin")); 244 } 245 246 @Override 247 public void actionPerformed(ActionEvent e) { 248 if (!isEnabled()) 249 return; 250 OsmPrimitive p = getPrimitiveToZoom(); 251 if (p != null) { 252 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 253 if (editLayer != null) { 254 editLayer.data.setSelected(p.getPrimitiveId()); 255 AutoScaleAction.autoScale("selection"); 256 } 257 } 258 } 259 260 public void setPrimitiveId(PrimitiveId pid) { 261 this.primitiveId = pid; 262 updateEnabledState(); 263 } 264 265 protected OsmPrimitive getPrimitiveToZoom() { 266 if (primitiveId == null) 267 return null; 268 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 269 if (editLayer == null) 270 return null; 271 return editLayer.data.getPrimitiveById(primitiveId); 272 } 273 274 public void updateEnabledState() { 275 setEnabled(Main.getLayerManager().getEditLayer() != null && getPrimitiveToZoom() != null); 276 } 277 } 278 279 static class ShowHistoryAction extends AbstractAction { 280 private transient PrimitiveId primitiveId; 281 282 /** 283 * Constructs a new {@code ShowHistoryAction}. 284 */ 285 ShowHistoryAction() { 286 putValue(NAME, tr("Show history")); 287 putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node")); 288 putValue(SMALL_ICON, ImageProvider.get("dialogs", "history")); 289 } 290 291 @Override 292 public void actionPerformed(ActionEvent e) { 293 if (isEnabled()) { 294 run(); 295 } 296 } 297 298 public void setPrimitiveId(PrimitiveId pid) { 299 this.primitiveId = pid; 300 updateEnabledState(); 301 } 302 303 public void run() { 304 if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) { 305 Main.worker.submit(new HistoryLoadTask().add(primitiveId)); 306 } 307 Main.worker.submit(() -> { 308 final History h = HistoryDataSet.getInstance().getHistory(primitiveId); 309 if (h == null) 310 return; 311 GuiHelper.runInEDT(() -> HistoryBrowserDialogManager.getInstance().show(h)); 312 }); 313 } 314 315 public void updateEnabledState() { 316 setEnabled(primitiveId != null && !primitiveId.isNew()); 317 } 318 } 319 320 private static PrimitiveId primitiveIdAtRow(TableModel model, int row) { 321 DiffTableModel castedModel = (DiffTableModel) model; 322 Long id = (Long) castedModel.getValueAt(row, 0).value; 323 return id == null ? null : new SimplePrimitiveId(id, OsmPrimitiveType.NODE); 324 } 325 326 class InternalPopupMenuLauncher extends PopupMenuLauncher { 327 InternalPopupMenuLauncher() { 328 super(popupMenu); 329 } 330 331 @Override 332 protected int checkTableSelection(JTable table, Point p) { 333 int row = super.checkTableSelection(table, p); 334 popupMenu.prepare(primitiveIdAtRow(table.getModel(), row)); 335 return row; 336 } 337 } 338 339 static class DoubleClickAdapter extends MouseAdapter { 340 private final JTable table; 341 private final ShowHistoryAction showHistoryAction; 342 343 DoubleClickAdapter(JTable table) { 344 this.table = table; 345 showHistoryAction = new ShowHistoryAction(); 346 } 347 348 @Override 349 public void mouseClicked(MouseEvent e) { 350 if (e.getClickCount() < 2) 351 return; 352 int row = table.rowAtPoint(e.getPoint()); 353 if (row <= 0) 354 return; 355 PrimitiveId pid = primitiveIdAtRow(table.getModel(), row); 356 if (pid == null || pid.isNew()) 357 return; 358 showHistoryAction.setPrimitiveId(pid); 359 showHistoryAction.run(); 360 } 361 } 362}