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 * 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(newReversedChangeListener(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(newReversedChangeListener(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 TableModelListener newReversedChangeListener(final JTable table, final NodeListTableColumnModel columnModel) { 099 return new TableModelListener() { 100 private Boolean reversed; 101 private final String nonReversedText = tr("Nodes") + (table.getFont().canDisplay('\u25bc') ? " \u25bc" : " (1-n)"); 102 private final String reversedText = tr("Nodes") + (table.getFont().canDisplay('\u25b2') ? " \u25b2" : " (n-1)"); 103 104 @Override 105 public void tableChanged(TableModelEvent e) { 106 if (e.getSource() instanceof DiffTableModel) { 107 final DiffTableModel mod = (DiffTableModel) e.getSource(); 108 if (reversed == null || reversed != mod.isReversed()) { 109 reversed = mod.isReversed(); 110 columnModel.getColumn(0).setHeaderValue(reversed ? reversedText : nonReversedText); 111 table.getTableHeader().setToolTipText( 112 reversed ? tr("The nodes of this way are in reverse order") : null); 113 table.getTableHeader().repaint(); 114 } 115 } 116 } 117 }; 118 } 119 120 protected void build() { 121 setLayout(new GridBagLayout()); 122 GridBagConstraints gc = new GridBagConstraints(); 123 124 // --------------------------- 125 gc.gridx = 0; 126 gc.gridy = 0; 127 gc.gridwidth = 1; 128 gc.gridheight = 1; 129 gc.weightx = 0.5; 130 gc.weighty = 0.0; 131 gc.insets = new Insets(5, 5, 5, 0); 132 gc.fill = GridBagConstraints.HORIZONTAL; 133 gc.anchor = GridBagConstraints.FIRST_LINE_START; 134 referenceInfoPanel = new VersionInfoPanel(model, PointInTimeType.REFERENCE_POINT_IN_TIME); 135 add(referenceInfoPanel, gc); 136 137 gc.gridx = 1; 138 gc.gridy = 0; 139 gc.gridwidth = 1; 140 gc.gridheight = 1; 141 gc.fill = GridBagConstraints.HORIZONTAL; 142 gc.weightx = 0.5; 143 gc.weighty = 0.0; 144 gc.anchor = GridBagConstraints.FIRST_LINE_START; 145 currentInfoPanel = new VersionInfoPanel(model, PointInTimeType.CURRENT_POINT_IN_TIME); 146 add(currentInfoPanel, gc); 147 148 adjustmentSynchronizer = new AdjustmentSynchronizer(); 149 selectionSynchronizer = new SelectionSynchronizer(); 150 151 popupMenu = new NodeListPopupMenu(); 152 153 // --------------------------- 154 gc.gridx = 0; 155 gc.gridy = 1; 156 gc.gridwidth = 1; 157 gc.gridheight = 1; 158 gc.weightx = 0.5; 159 gc.weighty = 1.0; 160 gc.fill = GridBagConstraints.BOTH; 161 gc.anchor = GridBagConstraints.NORTHWEST; 162 add(embeddInScrollPane(buildReferenceNodeListTable()), gc); 163 164 gc.gridx = 1; 165 gc.gridy = 1; 166 gc.gridwidth = 1; 167 gc.gridheight = 1; 168 gc.weightx = 0.5; 169 gc.weighty = 1.0; 170 gc.fill = GridBagConstraints.BOTH; 171 gc.anchor = GridBagConstraints.NORTHWEST; 172 add(embeddInScrollPane(buildCurrentNodeListTable()), gc); 173 } 174 175 protected void unregisterAsObserver(HistoryBrowserModel model) { 176 if (currentInfoPanel != null) { 177 model.deleteObserver(currentInfoPanel); 178 } 179 if (referenceInfoPanel != null) { 180 model.deleteObserver(referenceInfoPanel); 181 } 182 } 183 184 protected void registerAsObserver(HistoryBrowserModel model) { 185 if (currentInfoPanel != null) { 186 model.addObserver(currentInfoPanel); 187 } 188 if (referenceInfoPanel != null) { 189 model.addObserver(referenceInfoPanel); 190 } 191 } 192 193 /** 194 * Sets the history browser model. 195 * @param model the history browser model 196 */ 197 public void setModel(HistoryBrowserModel model) { 198 if (this.model != null) { 199 unregisterAsObserver(model); 200 } 201 this.model = model; 202 if (this.model != null) { 203 registerAsObserver(model); 204 } 205 } 206 207 static class NodeListPopupMenu extends JPopupMenu { 208 private final ZoomToNodeAction zoomToNodeAction; 209 private final ShowHistoryAction showHistoryAction; 210 211 NodeListPopupMenu() { 212 zoomToNodeAction = new ZoomToNodeAction(); 213 add(zoomToNodeAction); 214 showHistoryAction = new ShowHistoryAction(); 215 add(showHistoryAction); 216 } 217 218 public void prepare(PrimitiveId pid) { 219 zoomToNodeAction.setPrimitiveId(pid); 220 zoomToNodeAction.updateEnabledState(); 221 222 showHistoryAction.setPrimitiveId(pid); 223 showHistoryAction.updateEnabledState(); 224 } 225 } 226 227 static class ZoomToNodeAction extends AbstractAction { 228 private transient PrimitiveId primitiveId; 229 230 /** 231 * Constructs a new {@code ZoomToNodeAction}. 232 */ 233 ZoomToNodeAction() { 234 putValue(NAME, tr("Zoom to node")); 235 putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer")); 236 putValue(SMALL_ICON, ImageProvider.get("dialogs", "zoomin")); 237 } 238 239 @Override 240 public void actionPerformed(ActionEvent e) { 241 if (!isEnabled()) 242 return; 243 OsmPrimitive p = getPrimitiveToZoom(); 244 if (p != null) { 245 OsmDataLayer editLayer = Main.main.getEditLayer(); 246 if (editLayer != null) { 247 editLayer.data.setSelected(p.getPrimitiveId()); 248 AutoScaleAction.autoScale("selection"); 249 } 250 } 251 } 252 253 public void setPrimitiveId(PrimitiveId pid) { 254 this.primitiveId = pid; 255 updateEnabledState(); 256 } 257 258 protected OsmPrimitive getPrimitiveToZoom() { 259 if (primitiveId == null) 260 return null; 261 OsmDataLayer editLayer = Main.main.getEditLayer(); 262 if (editLayer == null) 263 return null; 264 return editLayer.data.getPrimitiveById(primitiveId); 265 } 266 267 public void updateEnabledState() { 268 if (!Main.main.hasEditLayer()) { 269 setEnabled(false); 270 return; 271 } 272 setEnabled(getPrimitiveToZoom() != null); 273 } 274 } 275 276 static class ShowHistoryAction extends AbstractAction { 277 private transient PrimitiveId primitiveId; 278 279 /** 280 * Constructs a new {@code ShowHistoryAction}. 281 */ 282 ShowHistoryAction() { 283 putValue(NAME, tr("Show history")); 284 putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node")); 285 putValue(SMALL_ICON, ImageProvider.get("dialogs", "history")); 286 } 287 288 @Override 289 public void actionPerformed(ActionEvent e) { 290 if (isEnabled()) { 291 run(); 292 } 293 } 294 295 public void setPrimitiveId(PrimitiveId pid) { 296 this.primitiveId = pid; 297 updateEnabledState(); 298 } 299 300 public void run() { 301 if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) { 302 Main.worker.submit(new HistoryLoadTask().add(primitiveId)); 303 } 304 Runnable r = new Runnable() { 305 @Override 306 public void run() { 307 final History h = HistoryDataSet.getInstance().getHistory(primitiveId); 308 if (h == null) 309 return; 310 GuiHelper.runInEDT(new Runnable() { 311 @Override public void run() { 312 HistoryBrowserDialogManager.getInstance().show(h); 313 } 314 }); 315 } 316 }; 317 Main.worker.submit(r); 318 } 319 320 public void updateEnabledState() { 321 setEnabled(primitiveId != null && !primitiveId.isNew()); 322 } 323 } 324 325 private static PrimitiveId primitiveIdAtRow(TableModel model, int row) { 326 DiffTableModel castedModel = (DiffTableModel) model; 327 Long id = (Long) castedModel.getValueAt(row, 0).value; 328 return id == null ? null : new SimplePrimitiveId(id, OsmPrimitiveType.NODE); 329 } 330 331 class InternalPopupMenuLauncher extends PopupMenuLauncher { 332 InternalPopupMenuLauncher() { 333 super(popupMenu); 334 } 335 336 @Override 337 protected int checkTableSelection(JTable table, Point p) { 338 int row = super.checkTableSelection(table, p); 339 popupMenu.prepare(primitiveIdAtRow(table.getModel(), row)); 340 return row; 341 } 342 } 343 344 static class DoubleClickAdapter extends MouseAdapter { 345 private final JTable table; 346 private final ShowHistoryAction showHistoryAction; 347 348 DoubleClickAdapter(JTable table) { 349 this.table = table; 350 showHistoryAction = new ShowHistoryAction(); 351 } 352 353 @Override 354 public void mouseClicked(MouseEvent e) { 355 if (e.getClickCount() < 2) 356 return; 357 int row = table.rowAtPoint(e.getPoint()); 358 if (row <= 0) 359 return; 360 PrimitiveId pid = primitiveIdAtRow(table.getModel(), row); 361 if (pid == null || pid.isNew()) 362 return; 363 showHistoryAction.setPrimitiveId(pid); 364 showHistoryAction.run(); 365 } 366 } 367}