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.table.TableModel;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.actions.AutoScaleAction;
024import org.openstreetmap.josm.data.osm.OsmPrimitive;
025import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
026import org.openstreetmap.josm.data.osm.PrimitiveId;
027import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
028import org.openstreetmap.josm.data.osm.history.History;
029import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
030import org.openstreetmap.josm.gui.layer.OsmDataLayer;
031import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer;
032import org.openstreetmap.josm.gui.util.GuiHelper;
033import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
034import org.openstreetmap.josm.tools.ImageProvider;
035
036/**
037 * NodeListViewer is a UI component which displays the node list of two
038 * version of a {@link OsmPrimitive} in a {@link History}.
039 *
040 * <ul>
041 *   <li>on the left, it displays the node list for the version at {@link PointInTimeType#REFERENCE_POINT_IN_TIME}</li>
042 *   <li>on the right, it displays the node list for the version at {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li>
043 * </ul>
044 *
045 */
046public class NodeListViewer extends JPanel {
047
048    private HistoryBrowserModel model;
049    private VersionInfoPanel referenceInfoPanel;
050    private VersionInfoPanel currentInfoPanel;
051    private AdjustmentSynchronizer adjustmentSynchronizer;
052    private SelectionSynchronizer selectionSynchronizer;
053    private NodeListPopupMenu popupMenu;
054
055    protected JScrollPane embeddInScrollPane(JTable table) {
056        JScrollPane pane = new JScrollPane(table);
057        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
058        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
059        adjustmentSynchronizer.participateInSynchronizedScrolling(pane.getVerticalScrollBar());
060        return pane;
061    }
062
063    protected JTable buildReferenceNodeListTable() {
064        JTable table = new JTable(
065                model.getNodeListTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME),
066                new NodeListTableColumnModel()
067        );
068        table.setName("table.referencenodelisttable");
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    protected JTable buildCurrentNodeListTable() {
077        JTable table = new JTable(
078                model.getNodeListTableModel(PointInTimeType.CURRENT_POINT_IN_TIME),
079                new NodeListTableColumnModel()
080        );
081        table.setName("table.currentnodelisttable");
082        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
083        selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel());
084        table.addMouseListener(new InternalPopupMenuLauncher());
085        table.addMouseListener(new DoubleClickAdapter(table));
086        return table;
087    }
088
089    protected void build() {
090        setLayout(new GridBagLayout());
091        GridBagConstraints gc = new GridBagConstraints();
092
093        // ---------------------------
094        gc.gridx = 0;
095        gc.gridy = 0;
096        gc.gridwidth = 1;
097        gc.gridheight = 1;
098        gc.weightx = 0.5;
099        gc.weighty = 0.0;
100        gc.insets = new Insets(5,5,5,0);
101        gc.fill = GridBagConstraints.HORIZONTAL;
102        gc.anchor = GridBagConstraints.FIRST_LINE_START;
103        referenceInfoPanel = new VersionInfoPanel(model, PointInTimeType.REFERENCE_POINT_IN_TIME);
104        add(referenceInfoPanel,gc);
105
106        gc.gridx = 1;
107        gc.gridy = 0;
108        gc.gridwidth = 1;
109        gc.gridheight = 1;
110        gc.fill = GridBagConstraints.HORIZONTAL;
111        gc.weightx = 0.5;
112        gc.weighty = 0.0;
113        gc.anchor = GridBagConstraints.FIRST_LINE_START;
114        currentInfoPanel = new VersionInfoPanel(model, PointInTimeType.CURRENT_POINT_IN_TIME);
115        add(currentInfoPanel,gc);
116
117        adjustmentSynchronizer = new AdjustmentSynchronizer();
118        selectionSynchronizer = new SelectionSynchronizer();
119
120        popupMenu = new NodeListPopupMenu();
121
122        // ---------------------------
123        gc.gridx = 0;
124        gc.gridy = 1;
125        gc.gridwidth = 1;
126        gc.gridheight = 1;
127        gc.weightx = 0.5;
128        gc.weighty = 1.0;
129        gc.fill = GridBagConstraints.BOTH;
130        gc.anchor = GridBagConstraints.NORTHWEST;
131        add(embeddInScrollPane(buildReferenceNodeListTable()),gc);
132
133        gc.gridx = 1;
134        gc.gridy = 1;
135        gc.gridwidth = 1;
136        gc.gridheight = 1;
137        gc.weightx = 0.5;
138        gc.weighty = 1.0;
139        gc.fill = GridBagConstraints.BOTH;
140        gc.anchor = GridBagConstraints.NORTHWEST;
141        add(embeddInScrollPane(buildCurrentNodeListTable()),gc);
142    }
143
144    public NodeListViewer(HistoryBrowserModel model) {
145        setModel(model);
146        build();
147    }
148
149    protected void unregisterAsObserver(HistoryBrowserModel model) {
150        if (currentInfoPanel != null) {
151            model.deleteObserver(currentInfoPanel);
152        }
153        if (referenceInfoPanel != null) {
154            model.deleteObserver(referenceInfoPanel);
155        }
156    }
157    protected void registerAsObserver(HistoryBrowserModel model) {
158        if (currentInfoPanel != null) {
159            model.addObserver(currentInfoPanel);
160        }
161        if (referenceInfoPanel != null) {
162            model.addObserver(referenceInfoPanel);
163        }
164    }
165
166    public void setModel(HistoryBrowserModel model) {
167        if (this.model != null) {
168            unregisterAsObserver(model);
169        }
170        this.model = model;
171        if (this.model != null) {
172            registerAsObserver(model);
173        }
174    }
175
176    static class NodeListPopupMenu extends JPopupMenu {
177        private final ZoomToNodeAction zoomToNodeAction;
178        private final ShowHistoryAction showHistoryAction;
179
180        public NodeListPopupMenu() {
181            zoomToNodeAction = new ZoomToNodeAction();
182            add(zoomToNodeAction);
183            showHistoryAction = new ShowHistoryAction();
184            add(showHistoryAction);
185        }
186
187        public void prepare(PrimitiveId pid){
188            zoomToNodeAction.setPrimitiveId(pid);
189            zoomToNodeAction.updateEnabledState();
190
191            showHistoryAction.setPrimitiveId(pid);
192            showHistoryAction.updateEnabledState();
193        }
194    }
195
196    static class ZoomToNodeAction extends AbstractAction {
197        private PrimitiveId primitiveId;
198
199        public ZoomToNodeAction() {
200            putValue(NAME, tr("Zoom to node"));
201            putValue(SHORT_DESCRIPTION, tr("Zoom to this node in the current data layer"));
202            putValue(SMALL_ICON, ImageProvider.get("dialogs", "zoomin"));
203        }
204
205        @Override
206        public void actionPerformed(ActionEvent e) {
207            if (!isEnabled()) return;
208            OsmPrimitive p = getPrimitiveToZoom();
209            if (p != null) {
210                OsmDataLayer editLayer = Main.main.getEditLayer();
211                if (editLayer != null) {
212                    editLayer.data.setSelected(p.getPrimitiveId());
213                    AutoScaleAction.autoScale("selection");
214                }
215            }
216        }
217
218        public void setPrimitiveId(PrimitiveId pid) {
219            this.primitiveId = pid;
220            updateEnabledState();
221        }
222
223        protected OsmPrimitive getPrimitiveToZoom() {
224            if (primitiveId == null) return null;
225            OsmDataLayer editLayer = Main.main.getEditLayer();
226            if (editLayer == null) return null;
227            OsmPrimitive p = editLayer.data.getPrimitiveById(primitiveId);
228            return p;
229        }
230
231        public void updateEnabledState() {
232            if (!Main.main.hasEditLayer()) {
233                setEnabled(false);
234                return;
235            }
236            setEnabled(getPrimitiveToZoom() != null);
237        }
238    }
239
240    static class ShowHistoryAction extends AbstractAction {
241        private PrimitiveId primitiveId;
242
243        public ShowHistoryAction() {
244            putValue(NAME, tr("Show history"));
245            putValue(SHORT_DESCRIPTION, tr("Open a history browser with the history of this node"));
246            putValue(SMALL_ICON, ImageProvider.get("dialogs", "history"));
247        }
248
249        @Override
250        public void actionPerformed(ActionEvent e) {
251            if (!isEnabled()) return;
252            run();
253        }
254
255        public void setPrimitiveId(PrimitiveId pid) {
256            this.primitiveId = pid;
257            updateEnabledState();
258        }
259
260        public void run() {
261            if (HistoryDataSet.getInstance().getHistory(primitiveId) == null) {
262                Main.worker.submit(new HistoryLoadTask().add(primitiveId));
263            }
264            Runnable r = new Runnable() {
265                @Override
266                public void run() {
267                    final History h = HistoryDataSet.getInstance().getHistory(primitiveId);
268                    if (h == null)
269                        return;
270                    GuiHelper.runInEDT(new Runnable() {
271                        @Override public void run() {
272                            HistoryBrowserDialogManager.getInstance().show(h);
273                        }
274                    });
275                }
276            };
277            Main.worker.submit(r);
278        }
279
280        public void updateEnabledState() {
281            setEnabled(primitiveId != null && primitiveId.getUniqueId() > 0);
282        }
283    }
284
285    static private PrimitiveId primitiveIdAtRow(TableModel model, int row) {
286        DiffTableModel castedModel = (DiffTableModel) model;
287        Long id = (Long)castedModel.getValueAt(row, 0).value;
288        if(id == null) return null;
289        return new SimplePrimitiveId(id, OsmPrimitiveType.NODE);
290    }
291
292    class InternalPopupMenuLauncher extends PopupMenuLauncher {
293        public InternalPopupMenuLauncher() {
294            super(popupMenu);
295        }
296
297        @Override protected int checkTableSelection(JTable table, Point p) {
298            int row = super.checkTableSelection(table, p);
299            popupMenu.prepare(primitiveIdAtRow(table.getModel(), row));
300            return row;
301        }
302    }
303
304    static class DoubleClickAdapter extends MouseAdapter {
305        private JTable table;
306        private ShowHistoryAction showHistoryAction;
307
308        public DoubleClickAdapter(JTable table) {
309            this.table = table;
310            showHistoryAction = new ShowHistoryAction();
311        }
312
313        @Override
314        public void mouseClicked(MouseEvent e) {
315            if (e.getClickCount() < 2) return;
316            int row = table.rowAtPoint(e.getPoint());
317            if(row <= 0) return;
318            PrimitiveId pid = primitiveIdAtRow(table.getModel(), row);
319            if (pid == null)
320                return;
321            showHistoryAction.setPrimitiveId(pid);
322            showHistoryAction.run();
323        }
324    }
325}