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 unregisterAsChangeListener(HistoryBrowserModel model) {
176        if (currentInfoPanel != null) {
177            model.removeChangeListener(currentInfoPanel);
178        }
179        if (referenceInfoPanel != null) {
180            model.removeChangeListener(referenceInfoPanel);
181        }
182    }
183
184    protected void registerAsChangeListener(HistoryBrowserModel model) {
185        if (currentInfoPanel != null) {
186            model.addChangeListener(currentInfoPanel);
187        }
188        if (referenceInfoPanel != null) {
189            model.addChangeListener(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            unregisterAsChangeListener(model);
200        }
201        this.model = model;
202        if (this.model != null) {
203            registerAsChangeListener(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}