001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Container;
008import java.awt.Dimension;
009import java.awt.FlowLayout;
010import java.awt.event.ActionEvent;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseEvent;
013import java.awt.event.WindowAdapter;
014import java.awt.event.WindowEvent;
015import java.util.Collection;
016import java.util.HashSet;
017import java.util.List;
018import java.util.Set;
019
020import javax.swing.AbstractAction;
021import javax.swing.DefaultListSelectionModel;
022import javax.swing.ImageIcon;
023import javax.swing.JComponent;
024import javax.swing.JFrame;
025import javax.swing.JOptionPane;
026import javax.swing.JPanel;
027import javax.swing.JPopupMenu;
028import javax.swing.JScrollPane;
029import javax.swing.JSplitPane;
030import javax.swing.JTabbedPane;
031import javax.swing.JTable;
032import javax.swing.JToolBar;
033import javax.swing.KeyStroke;
034import javax.swing.ListSelectionModel;
035import javax.swing.event.ListSelectionEvent;
036import javax.swing.event.ListSelectionListener;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.data.osm.Changeset;
040import org.openstreetmap.josm.data.osm.ChangesetCache;
041import org.openstreetmap.josm.gui.HelpAwareOptionPane;
042import org.openstreetmap.josm.gui.JosmUserIdentityManager;
043import org.openstreetmap.josm.gui.SideButton;
044import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog;
045import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryTask;
046import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
047import org.openstreetmap.josm.gui.help.HelpUtil;
048import org.openstreetmap.josm.gui.io.CloseChangesetTask;
049import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
050import org.openstreetmap.josm.io.ChangesetQuery;
051import org.openstreetmap.josm.io.OnlineResource;
052import org.openstreetmap.josm.tools.ImageProvider;
053import org.openstreetmap.josm.tools.WindowGeometry;
054
055/**
056 * ChangesetCacheManager manages the local cache of changesets
057 * retrieved from the OSM API. It displays both a table of the locally cached changesets
058 * and detail information about an individual changeset. It also provides actions for
059 * downloading, querying, closing changesets, in addition to removing changesets from
060 * the local cache.
061 *
062 */
063public class ChangesetCacheManager extends JFrame {
064
065    /** The changeset download icon **/
066    public static final ImageIcon DOWNLOAD_CONTENT_ICON = ImageProvider.get("dialogs/changeset", "downloadchangesetcontent");
067    /** The changeset update icon **/
068    public static final ImageIcon UPDATE_CONTENT_ICON   = ImageProvider.get("dialogs/changeset", "updatechangesetcontent");
069
070    /** the unique instance of the cache manager  */
071    private static ChangesetCacheManager instance;
072
073    /**
074     * Replies the unique instance of the changeset cache manager
075     *
076     * @return the unique instance of the changeset cache manager
077     */
078    public static ChangesetCacheManager getInstance() {
079        if (instance == null) {
080            instance = new ChangesetCacheManager();
081        }
082        return instance;
083    }
084
085    /**
086     * Hides and destroys the unique instance of the changeset cache
087     * manager.
088     *
089     */
090    public static void destroyInstance() {
091        if (instance != null) {
092            instance.setVisible(true);
093            instance.dispose();
094            instance = null;
095        }
096    }
097
098    private ChangesetCacheManagerModel model;
099    private JSplitPane spContent;
100    private boolean needsSplitPaneAdjustment;
101
102    private RemoveFromCacheAction actRemoveFromCacheAction;
103    private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction;
104    private DownloadSelectedChangesetsAction actDownloadSelectedChangesets;
105    private DownloadSelectedChangesetContentAction actDownloadSelectedContent;
106    private JTable tblChangesets;
107
108    /**
109     * Creates the various models required
110     */
111    protected void buildModel() {
112        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
113        selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
114        model = new ChangesetCacheManagerModel(selectionModel);
115
116        actRemoveFromCacheAction = new RemoveFromCacheAction();
117        actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction();
118        actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction();
119        actDownloadSelectedContent = new DownloadSelectedChangesetContentAction();
120    }
121
122    /**
123     * builds the toolbar panel in the heading of the dialog
124     *
125     * @return the toolbar panel
126     */
127    protected JPanel buildToolbarPanel() {
128        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
129
130        SideButton btn = new SideButton(new QueryAction());
131        pnl.add(btn);
132        pnl.add(new SingleChangesetDownloadPanel());
133        pnl.add(new SideButton(new DownloadMyChangesets()));
134
135        return pnl;
136    }
137
138    /**
139     * builds the button panel in the footer of the dialog
140     *
141     * @return the button row pane
142     */
143    protected JPanel buildButtonPanel() {
144        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
145
146        //-- cancel and close action
147        pnl.add(new SideButton(new CancelAction()));
148
149        //-- help action
150        pnl.add(new SideButton(
151                new ContextSensitiveHelpAction(
152                        HelpUtil.ht("/Dialog/ChangesetCacheManager"))
153        )
154        );
155
156        return pnl;
157    }
158
159    /**
160     * Builds the panel with the changeset details
161     *
162     * @return the panel with the changeset details
163     */
164    protected JPanel buildChangesetDetailPanel() {
165        JPanel pnl = new JPanel(new BorderLayout());
166        JTabbedPane tp = new JTabbedPane();
167
168        // -- add the details panel
169        ChangesetDetailPanel pnlChangesetDetail = new ChangesetDetailPanel();
170        tp.add(pnlChangesetDetail);
171        model.addPropertyChangeListener(pnlChangesetDetail);
172
173        // -- add the tags panel
174        ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel();
175        tp.add(pnlChangesetTags);
176        model.addPropertyChangeListener(pnlChangesetTags);
177
178        // -- add the panel for the changeset content
179        ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel();
180        tp.add(pnlChangesetContent);
181        model.addPropertyChangeListener(pnlChangesetContent);
182
183        // -- add the panel for the changeset discussion
184        ChangesetDiscussionPanel pnlChangesetDiscussion = new ChangesetDiscussionPanel();
185        tp.add(pnlChangesetDiscussion);
186        model.addPropertyChangeListener(pnlChangesetDiscussion);
187
188        tp.setTitleAt(0, tr("Properties"));
189        tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
190        tp.setTitleAt(1, tr("Tags"));
191        tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
192        tp.setTitleAt(2, tr("Content"));
193        tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
194        tp.setTitleAt(3, tr("Discussion"));
195        tp.setToolTipTextAt(3, tr("Display the public discussion around this changeset"));
196
197        pnl.add(tp, BorderLayout.CENTER);
198        return pnl;
199    }
200
201    /**
202     * builds the content panel of the dialog
203     *
204     * @return the content panel
205     */
206    protected JPanel buildContentPanel() {
207        JPanel pnl = new JPanel(new BorderLayout());
208
209        spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
210        spContent.setLeftComponent(buildChangesetTablePanel());
211        spContent.setRightComponent(buildChangesetDetailPanel());
212        spContent.setOneTouchExpandable(true);
213        spContent.setDividerLocation(0.5);
214
215        pnl.add(spContent, BorderLayout.CENTER);
216        return pnl;
217    }
218
219    /**
220     * Builds the table with actions which can be applied to the currently visible changesets
221     * in the changeset table.
222     *
223     * @return changset actions panel
224     */
225    protected JPanel buildChangesetTableActionPanel() {
226        JPanel pnl = new JPanel(new BorderLayout());
227
228        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
229        tb.setFloatable(false);
230
231        // -- remove from cache action
232        model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
233        tb.add(actRemoveFromCacheAction);
234
235        // -- close selected changesets action
236        model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
237        tb.add(actCloseSelectedChangesetsAction);
238
239        // -- download selected changesets
240        model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
241        tb.add(actDownloadSelectedChangesets);
242
243        // -- download the content of the selected changesets
244        model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
245        tb.add(actDownloadSelectedContent);
246
247        pnl.add(tb, BorderLayout.CENTER);
248        return pnl;
249    }
250
251    /**
252     * Builds the panel with the table of changesets
253     *
254     * @return the panel with the table of changesets
255     */
256    protected JPanel buildChangesetTablePanel() {
257        JPanel pnl = new JPanel(new BorderLayout());
258        tblChangesets = new JTable(
259                model,
260                new ChangesetCacheTableColumnModel(),
261                model.getSelectionModel()
262        );
263        tblChangesets.addMouseListener(new MouseEventHandler());
264        tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "showDetails");
265        tblChangesets.getActionMap().put("showDetails", new ShowDetailAction());
266        model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer());
267
268        // activate DEL on the table
269        tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "removeFromCache");
270        tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
271
272        pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
273        pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
274        return pnl;
275    }
276
277    protected void build() {
278        setTitle(tr("Changeset Management Dialog"));
279        setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
280        Container cp = getContentPane();
281
282        cp.setLayout(new BorderLayout());
283
284        buildModel();
285        cp.add(buildToolbarPanel(), BorderLayout.NORTH);
286        cp.add(buildContentPanel(), BorderLayout.CENTER);
287        cp.add(buildButtonPanel(), BorderLayout.SOUTH);
288
289        // the help context
290        HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetCacheManager"));
291
292        // make the dialog respond to ESC
293        getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "cancelAndClose");
294        getRootPane().getActionMap().put("cancelAndClose", new CancelAction());
295
296        // install a window event handler
297        addWindowListener(new WindowEventHandler());
298    }
299
300    /**
301     * Constructs a new {@code ChangesetCacheManager}.
302     */
303    public ChangesetCacheManager() {
304        build();
305    }
306
307    @Override
308    public void setVisible(boolean visible) {
309        if (visible) {
310            new WindowGeometry(
311                    getClass().getName() + ".geometry",
312                    WindowGeometry.centerInWindow(
313                            getParent(),
314                            new Dimension(1000,600)
315                    )
316            ).applySafe(this);
317            needsSplitPaneAdjustment = true;
318            model.init();
319
320        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
321            model.tearDown();
322            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
323        }
324        super.setVisible(visible);
325    }
326
327    /**
328     * Handler for window events
329     *
330     */
331    class WindowEventHandler extends WindowAdapter {
332        @Override
333        public void windowClosing(WindowEvent e) {
334            new CancelAction().cancelAndClose();
335        }
336
337        @Override
338        public void windowActivated(WindowEvent arg0) {
339            if (needsSplitPaneAdjustment) {
340                spContent.setDividerLocation(0.5);
341                needsSplitPaneAdjustment = false;
342            }
343        }
344    }
345
346    /**
347     * the cancel / close action
348     */
349    static class CancelAction extends AbstractAction {
350        public CancelAction() {
351            putValue(NAME, tr("Close"));
352            putValue(SMALL_ICON, ImageProvider.get("cancel"));
353            putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
354        }
355
356        public void cancelAndClose() {
357            destroyInstance();
358        }
359
360        @Override
361        public void actionPerformed(ActionEvent arg0) {
362            cancelAndClose();
363        }
364    }
365
366    /**
367     * The action to query and download changesets
368     */
369    class QueryAction extends AbstractAction {
370        public QueryAction() {
371            putValue(NAME, tr("Query"));
372            putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
373            putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
374            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
375        }
376
377        @Override
378        public void actionPerformed(ActionEvent evt) {
379            ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this);
380            dialog.initForUserInput();
381            dialog.setVisible(true);
382            if (dialog.isCanceled())
383                return;
384
385            try {
386                ChangesetQuery query = dialog.getChangesetQuery();
387                if (query == null) return;
388                ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
389                ChangesetCacheManager.getInstance().runDownloadTask(task);
390            } catch (IllegalStateException e) {
391                JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
392            }
393        }
394    }
395
396    /**
397     * Removes the selected changesets from the local changeset cache
398     *
399     */
400    class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{
401        public RemoveFromCacheAction() {
402            putValue(NAME, tr("Remove from cache"));
403            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
404            putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
405            updateEnabledState();
406        }
407
408        @Override
409        public void actionPerformed(ActionEvent arg0) {
410            List<Changeset> selected = model.getSelectedChangesets();
411            ChangesetCache.getInstance().remove(selected);
412        }
413
414        protected void updateEnabledState() {
415            setEnabled(model.hasSelectedChangesets());
416        }
417
418        @Override
419        public void valueChanged(ListSelectionEvent e) {
420            updateEnabledState();
421
422        }
423    }
424
425    /**
426     * Closes the selected changesets
427     *
428     */
429    class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
430        public CloseSelectedChangesetsAction() {
431            putValue(NAME, tr("Close"));
432            putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
433            putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
434            updateEnabledState();
435        }
436
437        @Override
438        public void actionPerformed(ActionEvent arg0) {
439            List<Changeset> selected = model.getSelectedChangesets();
440            Main.worker.submit(new CloseChangesetTask(selected));
441        }
442
443        protected void updateEnabledState() {
444            List<Changeset> selected = model.getSelectedChangesets();
445            JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
446            for (Changeset cs: selected) {
447                if (cs.isOpen()) {
448                    if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
449                        setEnabled(true);
450                        return;
451                    }
452                    if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
453                        setEnabled(true);
454                        return;
455                    }
456                }
457            }
458            setEnabled(false);
459        }
460
461        @Override
462        public void valueChanged(ListSelectionEvent e) {
463            updateEnabledState();
464        }
465    }
466
467    /**
468     * Downloads the selected changesets
469     *
470     */
471    class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
472        public DownloadSelectedChangesetsAction() {
473            putValue(NAME, tr("Update changeset"));
474            putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset"));
475            putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
476            updateEnabledState();
477        }
478
479        @Override
480        public void actionPerformed(ActionEvent arg0) {
481            List<Changeset> selected = model.getSelectedChangesets();
482            ChangesetHeaderDownloadTask task =ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this,selected);
483            ChangesetCacheManager.getInstance().runDownloadTask(task);
484        }
485
486        protected void updateEnabledState() {
487            setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
488        }
489
490        @Override
491        public void valueChanged(ListSelectionEvent e) {
492            updateEnabledState();
493        }
494    }
495
496    /**
497     * Downloads the content of selected changesets from the OSM server
498     *
499     */
500    class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{
501        public DownloadSelectedChangesetContentAction() {
502            putValue(NAME, tr("Download changeset content"));
503            putValue(SMALL_ICON, DOWNLOAD_CONTENT_ICON);
504            putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
505            updateEnabledState();
506        }
507
508        @Override
509        public void actionPerformed(ActionEvent arg0) {
510            ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this,model.getSelectedChangesetIds());
511            ChangesetCacheManager.getInstance().runDownloadTask(task);
512        }
513
514        protected void updateEnabledState() {
515            setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
516        }
517
518        @Override
519        public void valueChanged(ListSelectionEvent e) {
520            updateEnabledState();
521        }
522    }
523
524    class ShowDetailAction extends AbstractAction {
525
526        public void showDetails() {
527            List<Changeset> selected = model.getSelectedChangesets();
528            if (selected.size() != 1) return;
529            model.setChangesetInDetailView(selected.get(0));
530        }
531
532        @Override
533        public void actionPerformed(ActionEvent arg0) {
534            showDetails();
535        }
536    }
537
538    class DownloadMyChangesets extends AbstractAction {
539        public DownloadMyChangesets() {
540            putValue(NAME, tr("My changesets"));
541            putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset"));
542            putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
543            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
544        }
545
546        protected void alertAnonymousUser() {
547            HelpAwareOptionPane.showOptionDialog(
548                    ChangesetCacheManager.this,
549                    tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
550                            + "your changesets from the OSM server unless you enter your OSM user name<br>"
551                            + "in the JOSM preferences.</html>"
552                    ),
553                    tr("Warning"),
554                    JOptionPane.WARNING_MESSAGE,
555                    HelpUtil.ht("/Dialog/ChangesetCacheManager#CanDownloadMyChangesets")
556            );
557        }
558
559        @Override
560        public void actionPerformed(ActionEvent arg0) {
561            JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
562            if (im.isAnonymous()) {
563                alertAnonymousUser();
564                return;
565            }
566            ChangesetQuery query = new ChangesetQuery();
567            if (im.isFullyIdentified()) {
568                query = query.forUser(im.getUserId());
569            } else {
570                query = query.forUser(im.getUserName());
571            }
572            ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
573            ChangesetCacheManager.getInstance().runDownloadTask(task);
574        }
575    }
576
577    class MouseEventHandler extends PopupMenuLauncher {
578
579        public MouseEventHandler() {
580            super(new ChangesetTablePopupMenu());
581        }
582
583        @Override
584        public void mouseClicked(MouseEvent evt) {
585            if (isDoubleClick(evt)) {
586                new ShowDetailAction().showDetails();
587            }
588        }
589    }
590
591    class ChangesetTablePopupMenu extends JPopupMenu {
592        public ChangesetTablePopupMenu() {
593            add(actRemoveFromCacheAction);
594            add(actCloseSelectedChangesetsAction);
595            add(actDownloadSelectedChangesets);
596            add(actDownloadSelectedContent);
597        }
598    }
599
600    class ChangesetDetailViewSynchronizer implements ListSelectionListener {
601        @Override
602        public void valueChanged(ListSelectionEvent e) {
603            List<Changeset> selected = model.getSelectedChangesets();
604            if (selected.size() == 1) {
605                model.setChangesetInDetailView(selected.get(0));
606            } else {
607                model.setChangesetInDetailView(null);
608            }
609        }
610    }
611
612    /**
613     * Selects the changesets  in <code>changests</code>, provided the
614     * respective changesets are already present in the local changeset cache.
615     *
616     * @param changesets the collection of changesets. If {@code null}, the
617     * selection is cleared.
618     */
619    public void setSelectedChangesets(Collection<Changeset> changesets) {
620        model.setSelectedChangesets(changesets);
621        int idx = model.getSelectionModel().getMinSelectionIndex();
622        if (idx < 0) return;
623        tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true));
624        repaint();
625    }
626
627    /**
628     * Selects the changesets with the ids in <code>ids</code>, provided the
629     * respective changesets are already present in the local changeset cache.
630     *
631     * @param ids the collection of ids. If null, the selection is cleared.
632     */
633    public void setSelectedChangesetsById(Collection<Integer> ids) {
634        if (ids == null) {
635            setSelectedChangesets(null);
636            return;
637        }
638        Set<Changeset> toSelect = new HashSet<>();
639        ChangesetCache cc = ChangesetCache.getInstance();
640        for (int id: ids) {
641            if (cc.contains(id)) {
642                toSelect.add(cc.get(id));
643            }
644        }
645        setSelectedChangesets(toSelect);
646    }
647
648    /**
649     * Runs the given changeset download task.
650     * @param task The changeset download task to run
651     */
652    public void runDownloadTask(final ChangesetDownloadTask task) {
653        Main.worker.submit(task);
654        Main.worker.submit(new Runnable() {
655            @Override
656            public void run() {
657                if (task.isCanceled() || task.isFailed()) return;
658                setSelectedChangesets(task.getDownloadedChangesets());
659            }
660        });
661    }
662}