001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.FlowLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.ComponentAdapter;
011import java.awt.event.ComponentEvent;
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.HashSet;
017import java.util.List;
018import java.util.Set;
019
020import javax.swing.AbstractAction;
021import javax.swing.BorderFactory;
022import javax.swing.DefaultListSelectionModel;
023import javax.swing.JButton;
024import javax.swing.JOptionPane;
025import javax.swing.JPanel;
026import javax.swing.JPopupMenu;
027import javax.swing.JScrollPane;
028import javax.swing.JSeparator;
029import javax.swing.JTable;
030import javax.swing.JToolBar;
031import javax.swing.event.ListSelectionEvent;
032import javax.swing.event.ListSelectionListener;
033
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.actions.AutoScaleAction;
036import org.openstreetmap.josm.data.osm.Changeset;
037import org.openstreetmap.josm.data.osm.OsmPrimitive;
038import org.openstreetmap.josm.data.osm.history.History;
039import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
040import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
041import org.openstreetmap.josm.gui.HelpAwareOptionPane;
042import org.openstreetmap.josm.gui.MapView;
043import org.openstreetmap.josm.gui.MapView.EditLayerChangeListener;
044import org.openstreetmap.josm.gui.help.HelpUtil;
045import org.openstreetmap.josm.gui.history.HistoryBrowserDialogManager;
046import org.openstreetmap.josm.gui.history.HistoryLoadTask;
047import org.openstreetmap.josm.gui.layer.OsmDataLayer;
048import org.openstreetmap.josm.gui.util.GuiHelper;
049import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
050import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
051import org.openstreetmap.josm.tools.BugReportExceptionHandler;
052import org.openstreetmap.josm.tools.ImageProvider;
053
054/**
055 * The panel which displays the content of a changeset in a scollable table.
056 *
057 * It listens to property change events for {@link ChangesetCacheManagerModel#CHANGESET_IN_DETAIL_VIEW_PROP}
058 * and updates its view accordingly.
059 *
060 */
061public class ChangesetContentPanel extends JPanel implements PropertyChangeListener {
062
063    private ChangesetContentTableModel model;
064    private Changeset currentChangeset;
065
066    private DownloadChangesetContentAction actDownloadContentAction;
067    private ShowHistoryAction actShowHistory;
068    private SelectInCurrentLayerAction actSelectInCurrentLayerAction;
069    private ZoomInCurrentLayerAction actZoomInCurrentLayerAction;
070
071    private final HeaderPanel pnlHeader = new HeaderPanel();
072
073    protected void buildModels() {
074        DefaultListSelectionModel selectionModel =new DefaultListSelectionModel();
075        model = new ChangesetContentTableModel(selectionModel);
076        actDownloadContentAction = new DownloadChangesetContentAction();
077        actDownloadContentAction.initProperties(currentChangeset);
078        actShowHistory = new ShowHistoryAction();
079        model.getSelectionModel().addListSelectionListener(actShowHistory);
080
081        actSelectInCurrentLayerAction = new SelectInCurrentLayerAction();
082        model.getSelectionModel().addListSelectionListener(actSelectInCurrentLayerAction);
083        MapView.addEditLayerChangeListener(actSelectInCurrentLayerAction);
084
085        actZoomInCurrentLayerAction = new ZoomInCurrentLayerAction();
086        model.getSelectionModel().addListSelectionListener(actZoomInCurrentLayerAction);
087        MapView.addEditLayerChangeListener(actZoomInCurrentLayerAction);
088
089        addComponentListener(
090                new ComponentAdapter() {
091                    @Override
092                    public void componentHidden(ComponentEvent e) {
093                        // make sure the listener is unregistered when the panel becomes
094                        // invisible
095                        MapView.removeEditLayerChangeListener(actSelectInCurrentLayerAction);
096                        MapView.removeEditLayerChangeListener(actZoomInCurrentLayerAction);
097                    }
098                }
099        );
100    }
101
102    protected JPanel buildContentPanel() {
103        JPanel pnl = new JPanel(new BorderLayout());
104        JTable tblContent = new JTable(
105                model,
106                new ChangesetContentTableColumnModel(),
107                model.getSelectionModel()
108        );
109        tblContent.addMouseListener(new PopupMenuLauncher(new ChangesetContentTablePopupMenu()));
110        pnl.add(new JScrollPane(tblContent), BorderLayout.CENTER);
111        return pnl;
112    }
113
114    protected JPanel buildActionButtonPanel() {
115        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
116        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
117        tb.setFloatable(false);
118
119        tb.add(actDownloadContentAction);
120        tb.add(actShowHistory);
121        tb.add(actSelectInCurrentLayerAction);
122        tb.add(actZoomInCurrentLayerAction);
123
124        pnl.add(tb);
125        return pnl;
126    }
127
128    protected final void build() {
129        setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
130        setLayout(new BorderLayout());
131        buildModels();
132
133        add(pnlHeader, BorderLayout.NORTH);
134        add(buildActionButtonPanel(), BorderLayout.WEST);
135        add(buildContentPanel(), BorderLayout.CENTER);
136    }
137
138    /**
139     * Constructs a new {@code ChangesetContentPanel}.
140     */
141    public ChangesetContentPanel() {
142        build();
143    }
144
145    /**
146     * Replies the changeset content model
147     * @return The model
148     */
149    public ChangesetContentTableModel getModel() {
150        return model;
151    }
152
153    protected void setCurrentChangeset(Changeset cs) {
154        currentChangeset = cs;
155        if (cs == null) {
156            model.populate(null);
157        } else {
158            model.populate(cs.getContent());
159        }
160        actDownloadContentAction.initProperties(cs);
161        pnlHeader.setChangeset(cs);
162    }
163
164    /* ---------------------------------------------------------------------------- */
165    /* interface PropertyChangeListener                                             */
166    /* ---------------------------------------------------------------------------- */
167    @Override
168    public void propertyChange(PropertyChangeEvent evt) {
169        if(!evt.getPropertyName().equals(ChangesetCacheManagerModel.CHANGESET_IN_DETAIL_VIEW_PROP))
170            return;
171        Changeset cs = (Changeset)evt.getNewValue();
172        setCurrentChangeset(cs);
173    }
174
175    private final void alertNoPrimitivesTo(Collection<HistoryOsmPrimitive> primitives, String title, String helpTopic) {
176        HelpAwareOptionPane.showOptionDialog(
177                ChangesetContentPanel.this,
178                trn("<html>The selected object is not available in the current<br>"
179                        + "edit layer ''{0}''.</html>",
180                        "<html>None of the selected objects is available in the current<br>"
181                        + "edit layer ''{0}''.</html>",
182                        primitives.size(),
183                        Main.main.getEditLayer().getName()
184                ),
185                title, JOptionPane.WARNING_MESSAGE, helpTopic
186        );
187    }
188
189    /**
190     * Downloads/Updates the content of the changeset
191     *
192     */
193    class DownloadChangesetContentAction extends AbstractAction{
194        public DownloadChangesetContentAction() {
195            putValue(NAME, tr("Download content"));
196            putValue(SMALL_ICON, ChangesetCacheManager.DOWNLOAD_CONTENT_ICON);
197            putValue(SHORT_DESCRIPTION, tr("Download the changeset content from the OSM server"));
198        }
199
200        @Override
201        public void actionPerformed(ActionEvent evt) {
202            if (currentChangeset == null) return;
203            ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetContentPanel.this,currentChangeset.getId());
204            ChangesetCacheManager.getInstance().runDownloadTask(task);
205        }
206
207        public void initProperties(Changeset cs) {
208            if (cs == null) {
209                setEnabled(false);
210                return;
211            } else {
212                setEnabled(true);
213            }
214            if (cs.getContent() == null) {
215                putValue(NAME, tr("Download content"));
216                putValue(SMALL_ICON, ChangesetCacheManager.DOWNLOAD_CONTENT_ICON);
217                putValue(SHORT_DESCRIPTION, tr("Download the changeset content from the OSM server"));
218            } else {
219                putValue(NAME, tr("Update content"));
220                putValue(SMALL_ICON, ChangesetCacheManager.UPDATE_CONTENT_ICON);
221                putValue(SHORT_DESCRIPTION, tr("Update the changeset content from the OSM server"));
222            }
223        }
224    }
225
226    class ChangesetContentTablePopupMenu extends JPopupMenu {
227        public ChangesetContentTablePopupMenu() {
228            add(actDownloadContentAction);
229            add(actShowHistory);
230            add(new JSeparator());
231            add(actSelectInCurrentLayerAction);
232            add(actZoomInCurrentLayerAction);
233        }
234    }
235
236    class ShowHistoryAction extends AbstractAction implements ListSelectionListener {
237
238        private final class ShowHistoryTask implements Runnable {
239            private final Collection<HistoryOsmPrimitive> primitives;
240
241            private ShowHistoryTask(Collection<HistoryOsmPrimitive> primitives) {
242                this.primitives = primitives;
243            }
244
245            @Override
246            public void run() {
247                try {
248                    for (HistoryOsmPrimitive p : primitives) {
249                        final History h = HistoryDataSet.getInstance().getHistory(p.getPrimitiveId());
250                        if (h == null) {
251                            continue;
252                        }
253                        GuiHelper.runInEDT(new Runnable() {
254                            @Override
255                            public void run() {
256                                HistoryBrowserDialogManager.getInstance().show(h);
257                            }
258                        });
259                    }
260                } catch (final Exception e) {
261                    GuiHelper.runInEDT(new Runnable() {
262                        @Override
263                        public void run() {
264                            BugReportExceptionHandler.handleException(e);
265                        }
266                    });
267                }
268            }
269        }
270
271        public ShowHistoryAction() {
272            putValue(NAME, tr("Show history"));
273            putValue(SMALL_ICON, ImageProvider.get("dialogs", "history"));
274            putValue(SHORT_DESCRIPTION, tr("Download and show the history of the selected objects"));
275            updateEnabledState();
276        }
277
278        protected List<HistoryOsmPrimitive> filterPrimitivesWithUnloadedHistory(Collection<HistoryOsmPrimitive> primitives) {
279            List<HistoryOsmPrimitive> ret = new ArrayList<>(primitives.size());
280            for (HistoryOsmPrimitive p: primitives) {
281                if (HistoryDataSet.getInstance().getHistory(p.getPrimitiveId()) == null) {
282                    ret.add(p);
283                }
284            }
285            return ret;
286        }
287
288        public void showHistory(final Collection<HistoryOsmPrimitive> primitives) {
289
290            List<HistoryOsmPrimitive> toLoad = filterPrimitivesWithUnloadedHistory(primitives);
291            if (!toLoad.isEmpty()) {
292                HistoryLoadTask task = new HistoryLoadTask(ChangesetContentPanel.this);
293                for (HistoryOsmPrimitive p: toLoad) {
294                    task.add(p);
295                }
296                Main.worker.submit(task);
297            }
298
299            Main.worker.submit(new ShowHistoryTask(primitives));
300        }
301
302        protected final void updateEnabledState() {
303            setEnabled(model.hasSelectedPrimitives());
304        }
305
306        @Override
307        public void actionPerformed(ActionEvent arg0) {
308            Set<HistoryOsmPrimitive> selected = model.getSelectedPrimitives();
309            if (selected.isEmpty()) return;
310            showHistory(selected);
311        }
312
313        @Override
314        public void valueChanged(ListSelectionEvent e) {
315            updateEnabledState();
316        }
317    }
318
319    class SelectInCurrentLayerAction extends AbstractAction implements ListSelectionListener, EditLayerChangeListener{
320
321        public SelectInCurrentLayerAction() {
322            putValue(NAME, tr("Select in layer"));
323            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
324            putValue(SHORT_DESCRIPTION, tr("Select the corresponding primitives in the current data layer"));
325            updateEnabledState();
326        }
327
328        @Override
329        public void actionPerformed(ActionEvent arg0) {
330            if (!isEnabled())
331                return;
332            if (Main.main == null || !Main.main.hasEditLayer()) return;
333            OsmDataLayer layer = Main.main.getEditLayer();
334            Set<HistoryOsmPrimitive> selected = model.getSelectedPrimitives();
335            Set<OsmPrimitive> target = new HashSet<>();
336            for (HistoryOsmPrimitive p : model.getSelectedPrimitives()) {
337                OsmPrimitive op = layer.data.getPrimitiveById(p.getPrimitiveId());
338                if (op != null) {
339                    target.add(op);
340                }
341            }
342            if (target.isEmpty()) {
343                alertNoPrimitivesTo(selected, tr("Nothing to select"),
344                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer"));
345                return;
346            }
347            layer.data.setSelected(target);
348        }
349
350        public final void updateEnabledState() {
351            if (Main.main == null || !Main.main.hasEditLayer()) {
352                setEnabled(false);
353                return;
354            }
355            setEnabled(model.hasSelectedPrimitives());
356        }
357
358        @Override
359        public void valueChanged(ListSelectionEvent e) {
360            updateEnabledState();
361        }
362
363        @Override
364        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
365            updateEnabledState();
366        }
367    }
368
369    class ZoomInCurrentLayerAction extends AbstractAction implements ListSelectionListener, EditLayerChangeListener{
370
371        public ZoomInCurrentLayerAction() {
372            putValue(NAME, tr("Zoom to in layer"));
373            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
374            putValue(SHORT_DESCRIPTION, tr("Zoom to the corresponding objects in the current data layer"));
375            updateEnabledState();
376        }
377
378        @Override
379        public void actionPerformed(ActionEvent arg0) {
380            if (!isEnabled())
381                return;
382            if (Main.main == null || !Main.main.hasEditLayer()) return;
383            OsmDataLayer layer = Main.main.getEditLayer();
384            Set<HistoryOsmPrimitive> selected = model.getSelectedPrimitives();
385            Set<OsmPrimitive> target = new HashSet<>();
386            for (HistoryOsmPrimitive p : model.getSelectedPrimitives()) {
387                OsmPrimitive op = layer.data.getPrimitiveById(p.getPrimitiveId());
388                if (op != null) {
389                    target.add(op);
390                }
391            }
392            if (target.isEmpty()) {
393                alertNoPrimitivesTo(selected, tr("Nothing to zoom to"),
394                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo"));
395                return;
396            }
397            layer.data.setSelected(target);
398            AutoScaleAction.zoomToSelection();
399        }
400
401        public final void updateEnabledState() {
402            if (Main.main == null || !Main.main.hasEditLayer()) {
403                setEnabled(false);
404                return;
405            }
406            setEnabled(model.hasSelectedPrimitives());
407        }
408
409        @Override
410        public void valueChanged(ListSelectionEvent e) {
411            updateEnabledState();
412        }
413
414        @Override
415        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
416            updateEnabledState();
417        }
418    }
419
420    private static class HeaderPanel extends JPanel {
421
422        private JMultilineLabel lblMessage;
423        private Changeset current;
424
425        protected final void build() {
426            setLayout(new FlowLayout(FlowLayout.LEFT));
427            lblMessage = new JMultilineLabel(
428                    tr("The content of this changeset is not downloaded yet.")
429            );
430            add(lblMessage);
431            add(new JButton(new DownloadAction()));
432
433        }
434
435        public HeaderPanel() {
436            build();
437        }
438
439        public void setChangeset(Changeset cs) {
440            setVisible(cs != null && cs.getContent() == null);
441            this.current = cs;
442        }
443
444        private class DownloadAction extends AbstractAction {
445            public DownloadAction() {
446                putValue(NAME, tr("Download now"));
447                putValue(SHORT_DESCRIPTION, tr("Download the changeset content"));
448                putValue(SMALL_ICON, ChangesetCacheManager.DOWNLOAD_CONTENT_ICON);
449            }
450
451            @Override
452            public void actionPerformed(ActionEvent evt) {
453                if (current == null) return;
454                ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(HeaderPanel.this, current.getId());
455                ChangesetCacheManager.getInstance().runDownloadTask(task);
456            }
457        }
458    }
459}