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.Point;
007import java.awt.event.ActionEvent;
008import java.awt.event.MouseAdapter;
009import java.awt.event.MouseEvent;
010
011import javax.swing.AbstractAction;
012import javax.swing.JPopupMenu;
013import javax.swing.JTable;
014import javax.swing.ListSelectionModel;
015import javax.swing.event.TableModelEvent;
016import javax.swing.event.TableModelListener;
017
018import org.openstreetmap.josm.actions.AutoScaleAction;
019import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
020import org.openstreetmap.josm.data.osm.IPrimitive;
021import org.openstreetmap.josm.data.osm.OsmData;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
024import org.openstreetmap.josm.data.osm.PrimitiveId;
025import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
026import org.openstreetmap.josm.data.osm.history.History;
027import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
028import org.openstreetmap.josm.gui.MainApplication;
029import org.openstreetmap.josm.gui.util.GuiHelper;
030import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
031import org.openstreetmap.josm.tools.ImageProvider;
032
033/**
034 * NodeListViewer is a UI component which displays the node list of two
035 * version of a {@link OsmPrimitive} in a {@link History}.
036 *
037 * <ul>
038 *   <li>on the left, it displays the node list for the version at {@link PointInTimeType#REFERENCE_POINT_IN_TIME}</li>
039 *   <li>on the right, it displays the node list for the version at {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li>
040 * </ul>
041 * @since 1709
042 */
043public class NodeListViewer extends HistoryViewerPanel {
044
045    /**
046     * Constructs a new {@code NodeListViewer}.
047     * @param model history browser model
048     */
049    public NodeListViewer(HistoryBrowserModel model) {
050        super(model);
051    }
052
053    @Override
054    protected JTable buildTable(PointInTimeType pointInTimeType) {
055        final DiffTableModel tableModel = model.getNodeListTableModel(pointInTimeType);
056        final NodeListTableColumnModel columnModel = new NodeListTableColumnModel();
057        final JTable table = new JTable(tableModel, columnModel);
058        tableModel.addTableModelListener(new ReversedChangeListener(table, columnModel));
059        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
060        selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel());
061        table.addMouseListener(new InternalPopupMenuLauncher());
062        table.addMouseListener(new DoubleClickAdapter(table));
063        return table;
064    }
065
066    static final class ReversedChangeListener implements TableModelListener {
067        private final NodeListTableColumnModel columnModel;
068        private final JTable table;
069        private Boolean reversed;
070        private final String nonReversedText;
071        private final String reversedText;
072
073        ReversedChangeListener(JTable table, NodeListTableColumnModel columnModel) {
074            this.columnModel = columnModel;
075            this.table = table;
076            nonReversedText = tr("Nodes") + (table.getFont().canDisplay('\u25bc') ? " \u25bc" : " (1-n)");
077            reversedText = tr("Nodes") + (table.getFont().canDisplay('\u25b2') ? " \u25b2" : " (n-1)");
078        }
079
080        @Override
081        public void tableChanged(TableModelEvent e) {
082            if (e.getSource() instanceof DiffTableModel) {
083                final DiffTableModel mod = (DiffTableModel) e.getSource();
084                if (reversed == null || reversed != mod.isReversed()) {
085                    reversed = mod.isReversed();
086                    columnModel.getColumn(0).setHeaderValue(reversed ? reversedText : nonReversedText);
087                    table.getTableHeader().setToolTipText(
088                            reversed ? tr("The nodes of this way are in reverse order") : null);
089                    table.getTableHeader().repaint();
090                }
091            }
092        }
093    }
094
095    static class NodeListPopupMenu extends JPopupMenu {
096        private final ZoomToNodeAction zoomToNodeAction;
097        private final ShowHistoryAction showHistoryAction;
098
099        NodeListPopupMenu() {
100            zoomToNodeAction = new ZoomToNodeAction();
101            add(zoomToNodeAction);
102            showHistoryAction = new ShowHistoryAction();
103            add(showHistoryAction);
104        }
105
106        public void prepare(PrimitiveId pid) {
107            zoomToNodeAction.setPrimitiveId(pid);
108            zoomToNodeAction.updateEnabledState();
109
110            showHistoryAction.setPrimitiveId(pid);
111            showHistoryAction.updateEnabledState();
112        }
113    }
114
115    static class ZoomToNodeAction extends AbstractAction {
116        private transient PrimitiveId primitiveId;
117
118        /**
119         * Constructs a new {@code ZoomToNodeAction}.
120         */
121        ZoomToNodeAction() {
122            putValue(NAME, tr("Zoom to node"));
123            putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer"));
124            new ImageProvider("dialogs", "zoomin").getResource().attachImageIcon(this, true);
125        }
126
127        @Override
128        public void actionPerformed(ActionEvent e) {
129            if (!isEnabled())
130                return;
131            IPrimitive p = getPrimitiveToZoom();
132            if (p != null) {
133                OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
134                if (ds != null) {
135                    ds.setSelected(p.getPrimitiveId());
136                    AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
137                }
138            }
139        }
140
141        public void setPrimitiveId(PrimitiveId pid) {
142            this.primitiveId = pid;
143            updateEnabledState();
144        }
145
146        protected IPrimitive getPrimitiveToZoom() {
147            if (primitiveId == null)
148                return null;
149            OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
150            if (ds == null)
151                return null;
152            return ds.getPrimitiveById(primitiveId);
153        }
154
155        public void updateEnabledState() {
156            setEnabled(MainApplication.getLayerManager().getActiveData() != null && getPrimitiveToZoom() != null);
157        }
158    }
159
160    static class ShowHistoryAction extends AbstractAction {
161        private transient PrimitiveId primitiveId;
162
163        /**
164         * Constructs a new {@code ShowHistoryAction}.
165         */
166        ShowHistoryAction() {
167            putValue(NAME, tr("Show history"));
168            putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node"));
169            new ImageProvider("dialogs", "history").getResource().attachImageIcon(this, true);
170        }
171
172        @Override
173        public void actionPerformed(ActionEvent e) {
174            if (isEnabled()) {
175                run();
176            }
177        }
178
179        public void setPrimitiveId(PrimitiveId pid) {
180            this.primitiveId = pid;
181            updateEnabledState();
182        }
183
184        public void run() {
185            if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) {
186                MainApplication.worker.submit(new HistoryLoadTask().add(primitiveId));
187            }
188            MainApplication.worker.submit(() -> {
189                final History h = HistoryDataSet.getInstance().getHistory(primitiveId);
190                if (h == null)
191                    return;
192                GuiHelper.runInEDT(() -> HistoryBrowserDialogManager.getInstance().show(h));
193            });
194        }
195
196        public void updateEnabledState() {
197            setEnabled(primitiveId != null && !primitiveId.isNew());
198        }
199    }
200
201    private static PrimitiveId primitiveIdAtRow(DiffTableModel model, int row) {
202        Long id = (Long) model.getValueAt(row, 0).value;
203        return id == null ? null : new SimplePrimitiveId(id, OsmPrimitiveType.NODE);
204    }
205
206    static class InternalPopupMenuLauncher extends PopupMenuLauncher {
207        InternalPopupMenuLauncher() {
208            super(new NodeListPopupMenu());
209        }
210
211        @Override
212        protected int checkTableSelection(JTable table, Point p) {
213            int row = super.checkTableSelection(table, p);
214            ((NodeListPopupMenu) menu).prepare(primitiveIdAtRow((DiffTableModel) table.getModel(), row));
215            return row;
216        }
217    }
218
219    static class DoubleClickAdapter extends MouseAdapter {
220        private final JTable table;
221        private final ShowHistoryAction showHistoryAction;
222
223        DoubleClickAdapter(JTable table) {
224            this.table = table;
225            showHistoryAction = new ShowHistoryAction();
226        }
227
228        @Override
229        public void mouseClicked(MouseEvent e) {
230            if (e.getClickCount() < 2)
231                return;
232            int row = table.rowAtPoint(e.getPoint());
233            if (row <= 0)
234                return;
235            PrimitiveId pid = primitiveIdAtRow((DiffTableModel) table.getModel(), row);
236            if (pid == null || pid.isNew())
237                return;
238            showHistoryAction.setPrimitiveId(pid);
239            showHistoryAction.run();
240        }
241    }
242}