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