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 JTable table;
335        private 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}