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