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