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