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 buildReferenceTable() {
055        return buildTable(PointInTimeType.REFERENCE_POINT_IN_TIME, "table.referencenodelisttable");
056    }
057
058    @Override
059    protected JTable buildCurrentTable() {
060        return buildTable(PointInTimeType.CURRENT_POINT_IN_TIME, "table.currentnodelisttable");
061    }
062
063    private JTable buildTable(PointInTimeType pointInTimeType, String name) {
064        final DiffTableModel tableModel = model.getNodeListTableModel(pointInTimeType);
065        final NodeListTableColumnModel columnModel = new NodeListTableColumnModel();
066        final JTable table = new JTable(tableModel, columnModel);
067        tableModel.addTableModelListener(new ReversedChangeListener(table, columnModel));
068        table.setName(name);
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    static final class ReversedChangeListener implements TableModelListener {
077        private final NodeListTableColumnModel columnModel;
078        private final JTable table;
079        private Boolean reversed;
080        private final String nonReversedText;
081        private final String reversedText;
082
083        ReversedChangeListener(JTable table, NodeListTableColumnModel columnModel) {
084            this.columnModel = columnModel;
085            this.table = table;
086            nonReversedText = tr("Nodes") + (table.getFont().canDisplay('\u25bc') ? " \u25bc" : " (1-n)");
087            reversedText = tr("Nodes") + (table.getFont().canDisplay('\u25b2') ? " \u25b2" : " (n-1)");
088        }
089
090        @Override
091        public void tableChanged(TableModelEvent e) {
092            if (e.getSource() instanceof DiffTableModel) {
093                final DiffTableModel mod = (DiffTableModel) e.getSource();
094                if (reversed == null || reversed != mod.isReversed()) {
095                    reversed = mod.isReversed();
096                    columnModel.getColumn(0).setHeaderValue(reversed ? reversedText : nonReversedText);
097                    table.getTableHeader().setToolTipText(
098                            reversed ? tr("The nodes of this way are in reverse order") : null);
099                    table.getTableHeader().repaint();
100                }
101            }
102        }
103    }
104
105    static class NodeListPopupMenu extends JPopupMenu {
106        private final ZoomToNodeAction zoomToNodeAction;
107        private final ShowHistoryAction showHistoryAction;
108
109        NodeListPopupMenu() {
110            zoomToNodeAction = new ZoomToNodeAction();
111            add(zoomToNodeAction);
112            showHistoryAction = new ShowHistoryAction();
113            add(showHistoryAction);
114        }
115
116        public void prepare(PrimitiveId pid) {
117            zoomToNodeAction.setPrimitiveId(pid);
118            zoomToNodeAction.updateEnabledState();
119
120            showHistoryAction.setPrimitiveId(pid);
121            showHistoryAction.updateEnabledState();
122        }
123    }
124
125    static class ZoomToNodeAction extends AbstractAction {
126        private transient PrimitiveId primitiveId;
127
128        /**
129         * Constructs a new {@code ZoomToNodeAction}.
130         */
131        ZoomToNodeAction() {
132            putValue(NAME, tr("Zoom to node"));
133            putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer"));
134            new ImageProvider("dialogs", "zoomin").getResource().attachImageIcon(this, true);
135        }
136
137        @Override
138        public void actionPerformed(ActionEvent e) {
139            if (!isEnabled())
140                return;
141            IPrimitive p = getPrimitiveToZoom();
142            if (p != null) {
143                OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
144                if (ds != null) {
145                    ds.setSelected(p.getPrimitiveId());
146                    AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
147                }
148            }
149        }
150
151        public void setPrimitiveId(PrimitiveId pid) {
152            this.primitiveId = pid;
153            updateEnabledState();
154        }
155
156        protected IPrimitive getPrimitiveToZoom() {
157            if (primitiveId == null)
158                return null;
159            OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
160            if (ds == null)
161                return null;
162            return ds.getPrimitiveById(primitiveId);
163        }
164
165        public void updateEnabledState() {
166            setEnabled(MainApplication.getLayerManager().getActiveData() != null && getPrimitiveToZoom() != null);
167        }
168    }
169
170    static class ShowHistoryAction extends AbstractAction {
171        private transient PrimitiveId primitiveId;
172
173        /**
174         * Constructs a new {@code ShowHistoryAction}.
175         */
176        ShowHistoryAction() {
177            putValue(NAME, tr("Show history"));
178            putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node"));
179            new ImageProvider("dialogs", "history").getResource().attachImageIcon(this, true);
180        }
181
182        @Override
183        public void actionPerformed(ActionEvent e) {
184            if (isEnabled()) {
185                run();
186            }
187        }
188
189        public void setPrimitiveId(PrimitiveId pid) {
190            this.primitiveId = pid;
191            updateEnabledState();
192        }
193
194        public void run() {
195            if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) {
196                MainApplication.worker.submit(new HistoryLoadTask().add(primitiveId));
197            }
198            MainApplication.worker.submit(() -> {
199                final History h = HistoryDataSet.getInstance().getHistory(primitiveId);
200                if (h == null)
201                    return;
202                GuiHelper.runInEDT(() -> HistoryBrowserDialogManager.getInstance().show(h));
203            });
204        }
205
206        public void updateEnabledState() {
207            setEnabled(primitiveId != null && !primitiveId.isNew());
208        }
209    }
210
211    private static PrimitiveId primitiveIdAtRow(DiffTableModel model, int row) {
212        Long id = (Long) model.getValueAt(row, 0).value;
213        return id == null ? null : new SimplePrimitiveId(id, OsmPrimitiveType.NODE);
214    }
215
216    static class InternalPopupMenuLauncher extends PopupMenuLauncher {
217        InternalPopupMenuLauncher() {
218            super(new NodeListPopupMenu());
219        }
220
221        @Override
222        protected int checkTableSelection(JTable table, Point p) {
223            int row = super.checkTableSelection(table, p);
224            ((NodeListPopupMenu) menu).prepare(primitiveIdAtRow((DiffTableModel) table.getModel(), row));
225            return row;
226        }
227    }
228
229    static class DoubleClickAdapter extends MouseAdapter {
230        private final JTable table;
231        private final ShowHistoryAction showHistoryAction;
232
233        DoubleClickAdapter(JTable table) {
234            this.table = table;
235            showHistoryAction = new ShowHistoryAction();
236        }
237
238        @Override
239        public void mouseClicked(MouseEvent e) {
240            if (e.getClickCount() < 2)
241                return;
242            int row = table.rowAtPoint(e.getPoint());
243            if (row <= 0)
244                return;
245            PrimitiveId pid = primitiveIdAtRow((DiffTableModel) table.getModel(), row);
246            if (pid == null || pid.isNew())
247                return;
248            showHistoryAction.setPrimitiveId(pid);
249            showHistoryAction.run();
250        }
251    }
252}