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