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