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