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;
019import java.util.stream.Collectors;
020
021import javax.swing.AbstractAction;
022import javax.swing.BorderFactory;
023import javax.swing.DefaultListSelectionModel;
024import javax.swing.JButton;
025import javax.swing.JOptionPane;
026import javax.swing.JPanel;
027import javax.swing.JPopupMenu;
028import javax.swing.JScrollPane;
029import javax.swing.JSeparator;
030import javax.swing.JTable;
031import javax.swing.JToolBar;
032import javax.swing.event.ListSelectionEvent;
033import javax.swing.event.ListSelectionListener;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.actions.AutoScaleAction;
037import org.openstreetmap.josm.actions.downloadtasks.ChangesetContentDownloadTask;
038import org.openstreetmap.josm.data.osm.Changeset;
039import org.openstreetmap.josm.data.osm.OsmPrimitive;
040import org.openstreetmap.josm.data.osm.PrimitiveId;
041import org.openstreetmap.josm.data.osm.history.History;
042import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
043import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
044import org.openstreetmap.josm.gui.HelpAwareOptionPane;
045import org.openstreetmap.josm.gui.help.HelpUtil;
046import org.openstreetmap.josm.gui.history.HistoryBrowserDialogManager;
047import org.openstreetmap.josm.gui.history.HistoryLoadTask;
048import org.openstreetmap.josm.gui.io.DownloadPrimitivesWithReferrersTask;
049import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
050import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
051import org.openstreetmap.josm.gui.layer.OsmDataLayer;
052import org.openstreetmap.josm.gui.util.GuiHelper;
053import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
054import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
055import org.openstreetmap.josm.tools.ImageProvider;
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(() -> HistoryBrowserDialogManager.getInstance().show(h));
236                    }
237                } catch (final RuntimeException e) {
238                    GuiHelper.runInEDT(() -> BugReportExceptionHandler.handleException(e));
239                }
240            }
241        }
242
243        ShowHistoryAction() {
244            putValue(NAME, tr("Show history"));
245            new ImageProvider("dialogs", "history").getResource().attachImageIcon(this);
246            putValue(SHORT_DESCRIPTION, tr("Download and show the history of the selected objects"));
247            updateEnabledState();
248        }
249
250        protected List<HistoryOsmPrimitive> filterPrimitivesWithUnloadedHistory(Collection<HistoryOsmPrimitive> primitives) {
251            List<HistoryOsmPrimitive> ret = new ArrayList<>(primitives.size());
252            for (HistoryOsmPrimitive p: primitives) {
253                if (HistoryDataSet.getInstance().getHistory(p.getPrimitiveId()) == null) {
254                    ret.add(p);
255                }
256            }
257            return ret;
258        }
259
260        public void showHistory(final Collection<HistoryOsmPrimitive> primitives) {
261
262            List<HistoryOsmPrimitive> toLoad = filterPrimitivesWithUnloadedHistory(primitives);
263            if (!toLoad.isEmpty()) {
264                HistoryLoadTask task = new HistoryLoadTask(ChangesetContentPanel.this);
265                for (HistoryOsmPrimitive p: toLoad) {
266                    task.add(p);
267                }
268                Main.worker.submit(task);
269            }
270
271            Main.worker.submit(new ShowHistoryTask(primitives));
272        }
273
274        protected final void updateEnabledState() {
275            setEnabled(model.hasSelectedPrimitives());
276        }
277
278        @Override
279        public void actionPerformed(ActionEvent arg0) {
280            Set<HistoryOsmPrimitive> selected = model.getSelectedPrimitives();
281            if (selected.isEmpty()) return;
282            showHistory(selected);
283        }
284
285        @Override
286        public void valueChanged(ListSelectionEvent e) {
287            updateEnabledState();
288        }
289    }
290
291    class DownloadObjectAction extends AbstractAction implements ListSelectionListener {
292
293        DownloadObjectAction() {
294            putValue(NAME, tr("Download objects"));
295            putValue(SMALL_ICON, ImageProvider.get("downloadprimitive"));
296            putValue(SHORT_DESCRIPTION, tr("Download the current version of the selected objects"));
297            updateEnabledState();
298        }
299
300        @Override
301        public void actionPerformed(ActionEvent arg0) {
302            final List<PrimitiveId> primitiveIds = model.getSelectedPrimitives().stream().map(HistoryOsmPrimitive::getPrimitiveId)
303                    .collect(Collectors.toList());
304            Main.worker.submit(new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null));
305        }
306
307        protected final void updateEnabledState() {
308            setEnabled(model.hasSelectedPrimitives());
309        }
310
311        @Override
312        public void valueChanged(ListSelectionEvent e) {
313            updateEnabledState();
314        }
315    }
316
317    abstract class SelectionBasedAction extends AbstractAction implements ListSelectionListener, ActiveLayerChangeListener {
318
319        protected Set<OsmPrimitive> getTarget() {
320            if (!isEnabled()) {
321                return null;
322            }
323            OsmDataLayer layer = Main.getLayerManager().getEditLayer();
324            if (layer == null) {
325                return null;
326            }
327            Set<OsmPrimitive> target = new HashSet<>();
328            for (HistoryOsmPrimitive p : model.getSelectedPrimitives()) {
329                OsmPrimitive op = layer.data.getPrimitiveById(p.getPrimitiveId());
330                if (op != null) {
331                    target.add(op);
332                }
333            }
334            return target;
335        }
336
337        public final void updateEnabledState() {
338            setEnabled(Main.getLayerManager().getEditLayer() != null && model.hasSelectedPrimitives());
339        }
340
341        @Override
342        public void valueChanged(ListSelectionEvent e) {
343            updateEnabledState();
344        }
345
346        @Override
347        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
348            updateEnabledState();
349        }
350
351    }
352
353    class SelectInCurrentLayerAction extends SelectionBasedAction {
354
355        SelectInCurrentLayerAction() {
356            putValue(NAME, tr("Select in layer"));
357            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this);
358            putValue(SHORT_DESCRIPTION, tr("Select the corresponding primitives in the current data layer"));
359            updateEnabledState();
360        }
361
362        @Override
363        public void actionPerformed(ActionEvent arg0) {
364            final Set<OsmPrimitive> target = getTarget();
365            if (target == null) {
366                return;
367            } else if (target.isEmpty()) {
368                alertNoPrimitivesTo(model.getSelectedPrimitives(), tr("Nothing to select"),
369                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer"));
370                return;
371            }
372            Main.getLayerManager().getEditLayer().data.setSelected(target);
373        }
374    }
375
376    class ZoomInCurrentLayerAction extends SelectionBasedAction {
377
378        ZoomInCurrentLayerAction() {
379            putValue(NAME, tr("Zoom to in layer"));
380            new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this);
381            putValue(SHORT_DESCRIPTION, tr("Zoom to the corresponding objects in the current data layer"));
382            updateEnabledState();
383        }
384
385        @Override
386        public void actionPerformed(ActionEvent arg0) {
387            final Set<OsmPrimitive> target = getTarget();
388            if (target == null) {
389                return;
390            } else if (target.isEmpty()) {
391                alertNoPrimitivesTo(model.getSelectedPrimitives(), tr("Nothing to zoom to"),
392                        HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo"));
393                return;
394            }
395            Main.getLayerManager().getEditLayer().data.setSelected(target);
396            AutoScaleAction.zoomToSelection();
397        }
398    }
399
400    private static class HeaderPanel extends JPanel {
401
402        private transient Changeset current;
403
404        HeaderPanel() {
405            build();
406        }
407
408        protected final void build() {
409            setLayout(new FlowLayout(FlowLayout.LEFT));
410            add(new JMultilineLabel(tr("The content of this changeset is not downloaded yet.")));
411            add(new JButton(new DownloadAction()));
412
413        }
414
415        public void setChangeset(Changeset cs) {
416            setVisible(cs != null && cs.getContent() == null);
417            this.current = cs;
418        }
419
420        private class DownloadAction extends AbstractAction {
421            DownloadAction() {
422                putValue(NAME, tr("Download now"));
423                putValue(SHORT_DESCRIPTION, tr("Download the changeset content"));
424                new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this);
425            }
426
427            @Override
428            public void actionPerformed(ActionEvent evt) {
429                if (current == null) return;
430                ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(HeaderPanel.this, current.getId());
431                ChangesetCacheManager.getInstance().runDownloadTask(task);
432            }
433        }
434    }
435
436    @Override
437    public Changeset getCurrentChangeset() {
438        return currentChangeset;
439    }
440}