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        tp.setTitleAt(0, tr("Properties"));
184        tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
185        tp.setTitleAt(1, tr("Tags"));
186        tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
187        tp.setTitleAt(2, tr("Content"));
188        tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
189
190        pnl.add(tp, BorderLayout.CENTER);
191        return pnl;
192    }
193
194    /**
195     * builds the content panel of the dialog
196     *
197     * @return the content panel
198     */
199    protected JPanel buildContentPanel() {
200        JPanel pnl = new JPanel(new BorderLayout());
201
202        spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
203        spContent.setLeftComponent(buildChangesetTablePanel());
204        spContent.setRightComponent(buildChangesetDetailPanel());
205        spContent.setOneTouchExpandable(true);
206        spContent.setDividerLocation(0.5);
207
208        pnl.add(spContent, BorderLayout.CENTER);
209        return pnl;
210    }
211
212    /**
213     * Builds the table with actions which can be applied to the currently visible changesets
214     * in the changeset table.
215     *
216     * @return changset actions panel
217     */
218    protected JPanel buildChangesetTableActionPanel() {
219        JPanel pnl = new JPanel(new BorderLayout());
220
221        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
222        tb.setFloatable(false);
223
224        // -- remove from cache action
225        model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
226        tb.add(actRemoveFromCacheAction);
227
228        // -- close selected changesets action
229        model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
230        tb.add(actCloseSelectedChangesetsAction);
231
232        // -- download selected changesets
233        model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
234        tb.add(actDownloadSelectedChangesets);
235
236        // -- download the content of the selected changesets
237        model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
238        tb.add(actDownloadSelectedContent);
239
240        pnl.add(tb, BorderLayout.CENTER);
241        return pnl;
242    }
243
244    /**
245     * Builds the panel with the table of changesets
246     *
247     * @return the panel with the table of changesets
248     */
249    protected JPanel buildChangesetTablePanel() {
250        JPanel pnl = new JPanel(new BorderLayout());
251        tblChangesets = new JTable(
252                model,
253                new ChangesetCacheTableColumnModel(),
254                model.getSelectionModel()
255        );
256        tblChangesets.addMouseListener(new MouseEventHandler());
257        tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "showDetails");
258        tblChangesets.getActionMap().put("showDetails", new ShowDetailAction());
259        model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer());
260
261        // activate DEL on the table
262        tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "removeFromCache");
263        tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
264
265        pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
266        pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
267        return pnl;
268    }
269
270    protected void build() {
271        setTitle(tr("Changeset Management Dialog"));
272        setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
273        Container cp = getContentPane();
274
275        cp.setLayout(new BorderLayout());
276
277        buildModel();
278        cp.add(buildToolbarPanel(), BorderLayout.NORTH);
279        cp.add(buildContentPanel(), BorderLayout.CENTER);
280        cp.add(buildButtonPanel(), BorderLayout.SOUTH);
281
282        // the help context
283        HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetCacheManager"));
284
285        // make the dialog respond to ESC
286        getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "cancelAndClose");
287        getRootPane().getActionMap().put("cancelAndClose", new CancelAction());
288
289        // install a window event handler
290        addWindowListener(new WindowEventHandler());
291    }
292
293    /**
294     * Constructs a new {@code ChangesetCacheManager}.
295     */
296    public ChangesetCacheManager() {
297        build();
298    }
299
300    @Override
301    public void setVisible(boolean visible) {
302        if (visible) {
303            new WindowGeometry(
304                    getClass().getName() + ".geometry",
305                    WindowGeometry.centerInWindow(
306                            getParent(),
307                            new Dimension(1000,600)
308                    )
309            ).applySafe(this);
310            needsSplitPaneAdjustment = true;
311            model.init();
312
313        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
314            model.tearDown();
315            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
316        }
317        super.setVisible(visible);
318    }
319
320    /**
321     * Handler for window events
322     *
323     */
324    class WindowEventHandler extends WindowAdapter {
325        @Override
326        public void windowClosing(WindowEvent e) {
327            new CancelAction().cancelAndClose();
328        }
329
330        @Override
331        public void windowActivated(WindowEvent arg0) {
332            if (needsSplitPaneAdjustment) {
333                spContent.setDividerLocation(0.5);
334                needsSplitPaneAdjustment = false;
335            }
336        }
337    }
338
339    /**
340     * the cancel / close action
341     */
342    static class CancelAction extends AbstractAction {
343        public CancelAction() {
344            putValue(NAME, tr("Close"));
345            putValue(SMALL_ICON, ImageProvider.get("cancel"));
346            putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
347        }
348
349        public void cancelAndClose() {
350            destroyInstance();
351        }
352
353        @Override
354        public void actionPerformed(ActionEvent arg0) {
355            cancelAndClose();
356        }
357    }
358
359    /**
360     * The action to query and download changesets
361     */
362    class QueryAction extends AbstractAction {
363        public QueryAction() {
364            putValue(NAME, tr("Query"));
365            putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
366            putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
367            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
368        }
369
370        @Override
371        public void actionPerformed(ActionEvent evt) {
372            ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this);
373            dialog.initForUserInput();
374            dialog.setVisible(true);
375            if (dialog.isCanceled())
376                return;
377
378            try {
379                ChangesetQuery query = dialog.getChangesetQuery();
380                if (query == null) return;
381                ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
382                ChangesetCacheManager.getInstance().runDownloadTask(task);
383            } catch (IllegalStateException e) {
384                JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
385            }
386        }
387    }
388
389    /**
390     * Removes the selected changesets from the local changeset cache
391     *
392     */
393    class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{
394        public RemoveFromCacheAction() {
395            putValue(NAME, tr("Remove from cache"));
396            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
397            putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
398            updateEnabledState();
399        }
400
401        @Override
402        public void actionPerformed(ActionEvent arg0) {
403            List<Changeset> selected = model.getSelectedChangesets();
404            ChangesetCache.getInstance().remove(selected);
405        }
406
407        protected void updateEnabledState() {
408            setEnabled(model.hasSelectedChangesets());
409        }
410
411        @Override
412        public void valueChanged(ListSelectionEvent e) {
413            updateEnabledState();
414
415        }
416    }
417
418    /**
419     * Closes the selected changesets
420     *
421     */
422    class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
423        public CloseSelectedChangesetsAction() {
424            putValue(NAME, tr("Close"));
425            putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
426            putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
427            updateEnabledState();
428        }
429
430        @Override
431        public void actionPerformed(ActionEvent arg0) {
432            List<Changeset> selected = model.getSelectedChangesets();
433            Main.worker.submit(new CloseChangesetTask(selected));
434        }
435
436        protected void updateEnabledState() {
437            List<Changeset> selected = model.getSelectedChangesets();
438            JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
439            for (Changeset cs: selected) {
440                if (cs.isOpen()) {
441                    if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
442                        setEnabled(true);
443                        return;
444                    }
445                    if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
446                        setEnabled(true);
447                        return;
448                    }
449                }
450            }
451            setEnabled(false);
452        }
453
454        @Override
455        public void valueChanged(ListSelectionEvent e) {
456            updateEnabledState();
457        }
458    }
459
460    /**
461     * Downloads the selected changesets
462     *
463     */
464    class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
465        public DownloadSelectedChangesetsAction() {
466            putValue(NAME, tr("Update changeset"));
467            putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset"));
468            putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
469            updateEnabledState();
470        }
471
472        @Override
473        public void actionPerformed(ActionEvent arg0) {
474            List<Changeset> selected = model.getSelectedChangesets();
475            ChangesetHeaderDownloadTask task =ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this,selected);
476            ChangesetCacheManager.getInstance().runDownloadTask(task);
477        }
478
479        protected void updateEnabledState() {
480            setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
481        }
482
483        @Override
484        public void valueChanged(ListSelectionEvent e) {
485            updateEnabledState();
486        }
487    }
488
489    /**
490     * Downloads the content of selected changesets from the OSM server
491     *
492     */
493    class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{
494        public DownloadSelectedChangesetContentAction() {
495            putValue(NAME, tr("Download changeset content"));
496            putValue(SMALL_ICON, DOWNLOAD_CONTENT_ICON);
497            putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
498            updateEnabledState();
499        }
500
501        @Override
502        public void actionPerformed(ActionEvent arg0) {
503            ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this,model.getSelectedChangesetIds());
504            ChangesetCacheManager.getInstance().runDownloadTask(task);
505        }
506
507        protected void updateEnabledState() {
508            setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
509        }
510
511        @Override
512        public void valueChanged(ListSelectionEvent e) {
513            updateEnabledState();
514        }
515    }
516
517    class ShowDetailAction extends AbstractAction {
518
519        public void showDetails() {
520            List<Changeset> selected = model.getSelectedChangesets();
521            if (selected.size() != 1) return;
522            model.setChangesetInDetailView(selected.get(0));
523        }
524
525        @Override
526        public void actionPerformed(ActionEvent arg0) {
527            showDetails();
528        }
529    }
530
531    class DownloadMyChangesets extends AbstractAction {
532        public DownloadMyChangesets() {
533            putValue(NAME, tr("My changesets"));
534            putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset"));
535            putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
536            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
537        }
538
539        protected void alertAnonymousUser() {
540            HelpAwareOptionPane.showOptionDialog(
541                    ChangesetCacheManager.this,
542                    tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
543                            + "your changesets from the OSM server unless you enter your OSM user name<br>"
544                            + "in the JOSM preferences.</html>"
545                    ),
546                    tr("Warning"),
547                    JOptionPane.WARNING_MESSAGE,
548                    HelpUtil.ht("/Dialog/ChangesetCacheManager#CanDownloadMyChangesets")
549            );
550        }
551
552        @Override
553        public void actionPerformed(ActionEvent arg0) {
554            JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
555            if (im.isAnonymous()) {
556                alertAnonymousUser();
557                return;
558            }
559            ChangesetQuery query = new ChangesetQuery();
560            if (im.isFullyIdentified()) {
561                query = query.forUser(im.getUserId());
562            } else {
563                query = query.forUser(im.getUserName());
564            }
565            ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
566            ChangesetCacheManager.getInstance().runDownloadTask(task);
567        }
568    }
569
570    class MouseEventHandler extends PopupMenuLauncher {
571
572        public MouseEventHandler() {
573            super(new ChangesetTablePopupMenu());
574        }
575
576        @Override
577        public void mouseClicked(MouseEvent evt) {
578            if (isDoubleClick(evt)) {
579                new ShowDetailAction().showDetails();
580            }
581        }
582    }
583
584    class ChangesetTablePopupMenu extends JPopupMenu {
585        public ChangesetTablePopupMenu() {
586            add(actRemoveFromCacheAction);
587            add(actCloseSelectedChangesetsAction);
588            add(actDownloadSelectedChangesets);
589            add(actDownloadSelectedContent);
590        }
591    }
592
593    class ChangesetDetailViewSynchronizer implements ListSelectionListener {
594        @Override
595        public void valueChanged(ListSelectionEvent e) {
596            List<Changeset> selected = model.getSelectedChangesets();
597            if (selected.size() == 1) {
598                model.setChangesetInDetailView(selected.get(0));
599            } else {
600                model.setChangesetInDetailView(null);
601            }
602        }
603    }
604
605    /**
606     * Selects the changesets  in <code>changests</code>, provided the
607     * respective changesets are already present in the local changeset cache.
608     *
609     * @param changesets the collection of changesets. If {@code null}, the
610     * selection is cleared.
611     */
612    public void setSelectedChangesets(Collection<Changeset> changesets) {
613        model.setSelectedChangesets(changesets);
614        int idx = model.getSelectionModel().getMinSelectionIndex();
615        if (idx < 0) return;
616        tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true));
617        repaint();
618    }
619
620    /**
621     * Selects the changesets with the ids in <code>ids</code>, provided the
622     * respective changesets are already present in the local changeset cache.
623     *
624     * @param ids the collection of ids. If null, the selection is cleared.
625     */
626    public void setSelectedChangesetsById(Collection<Integer> ids) {
627        if (ids == null) {
628            setSelectedChangesets(null);
629            return;
630        }
631        Set<Changeset> toSelect = new HashSet<>();
632        ChangesetCache cc = ChangesetCache.getInstance();
633        for (int id: ids) {
634            if (cc.contains(id)) {
635                toSelect.add(cc.get(id));
636            }
637        }
638        setSelectedChangesets(toSelect);
639    }
640
641    /**
642     * Runs the given changeset download task.
643     * @param task The changeset download task to run
644     */
645    public void runDownloadTask(final ChangesetDownloadTask task) {
646        Main.worker.submit(task);
647        Main.worker.submit(new Runnable() {
648            @Override
649            public void run() {
650                if (task.isCanceled() || task.isFailed()) return;
651                setSelectedChangesets(task.getDownloadedChangesets());
652            }
653        });
654    }
655}