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