source: josm/trunk/src/org/openstreetmap/josm/gui/download/DownloadDialog.java @ 12652

Last change on this file since 12652 was 12652, checked in by michael2402, 2 months ago

Apply #15167: Merge OSM and overpass download dialog. Patch by bafonins

  • Property svn:eol-style set to native
File size: 21.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.download;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.FlowLayout;
11import java.awt.Graphics;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.InputEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.WindowAdapter;
17import java.awt.event.WindowEvent;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.List;
21import java.util.Optional;
22import java.util.stream.IntStream;
23
24import javax.swing.AbstractAction;
25import javax.swing.Icon;
26import javax.swing.JButton;
27import javax.swing.JCheckBox;
28import javax.swing.JComponent;
29import javax.swing.JDialog;
30import javax.swing.JLabel;
31import javax.swing.JPanel;
32import javax.swing.JSplitPane;
33import javax.swing.JTabbedPane;
34import javax.swing.KeyStroke;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.actions.ExpertToggleAction;
38import org.openstreetmap.josm.data.Bounds;
39import org.openstreetmap.josm.data.preferences.BooleanProperty;
40import org.openstreetmap.josm.data.preferences.IntegerProperty;
41import org.openstreetmap.josm.gui.MainApplication;
42import org.openstreetmap.josm.gui.MapView;
43import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
44import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
45import org.openstreetmap.josm.gui.help.HelpUtil;
46import org.openstreetmap.josm.gui.util.GuiHelper;
47import org.openstreetmap.josm.io.OnlineResource;
48import org.openstreetmap.josm.plugins.PluginHandler;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.InputMapUtils;
52import org.openstreetmap.josm.tools.Logging;
53import org.openstreetmap.josm.tools.OsmUrlToBounds;
54import org.openstreetmap.josm.tools.WindowGeometry;
55
56/**
57 * Dialog displayed to the user to download mapping data.
58 */
59public class DownloadDialog extends JDialog {
60
61    /**
62     * Preference properties
63     */
64    private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0);
65    private static final IntegerProperty DOWNLOAD_SOURCE_TAB = new IntegerProperty("download-source.tab", 0);
66    private static final IntegerProperty DIALOG_SPLIT = new IntegerProperty("download.split", 200);
67    private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false);
68    private static final BooleanProperty DOWNLOAD_NEWLAYER = new BooleanProperty("download.newlayer", false);
69    private static final BooleanProperty DOWNLOAD_ZOOMTODATA = new BooleanProperty("download.zoomtodata", true);
70
71    /** the unique instance of the download dialog */
72    private static DownloadDialog instance;
73
74    /**
75     * Replies the unique instance of the download dialog
76     *
77     * @return the unique instance of the download dialog
78     */
79    public static synchronized DownloadDialog getInstance() {
80        if (instance == null) {
81            instance = new DownloadDialog(Main.parent);
82        }
83        return instance;
84    }
85
86    protected final transient List<DownloadSource> downloadSources = new ArrayList<>();
87    protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>();
88    protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane();
89    protected final JTabbedPane downloadSourcesTab = new JTabbedPane();
90
91    protected JCheckBox cbNewLayer;
92    protected JCheckBox cbStartup;
93    protected JCheckBox cbZoomToDownloadedData;
94    protected SlippyMapChooser slippyMapChooser;
95    protected JPanel mainPanel;
96    protected JSplitPane dialogSplit;
97
98    /*
99     * Keep the reference globally to avoid having it garbage collected
100     */
101    protected final transient ExpertToggleAction.ExpertModeChangeListener expertListener =
102            getExpertModeListenerForDownloadSources();
103    protected transient Bounds currentBounds;
104    protected boolean canceled;
105
106    protected JButton btnDownload;
107    protected JButton btnCancel;
108    protected JButton btnHelp;
109
110    /**
111     * Builds the main panel of the dialog.
112     * @return The panel of the dialog.
113     */
114    protected final JPanel buildMainPanel() {
115        mainPanel = new JPanel(new GridBagLayout());
116
117        downloadSources.add(new OSMDownloadSource());
118        downloadSources.add(new OverpassDownloadSource());
119
120        // register all default download sources
121        for (int i = 0; i < downloadSources.size(); i++) {
122            downloadSources.get(i).addGui(this);
123        }
124
125        // must be created before hook
126        slippyMapChooser = new SlippyMapChooser();
127
128        // predefined download selections
129        downloadSelections.add(slippyMapChooser);
130        downloadSelections.add(new BookmarkSelection());
131        downloadSelections.add(new BoundingBoxSelection());
132        downloadSelections.add(new PlaceSelection());
133        downloadSelections.add(new TileSelection());
134
135        // add selections from plugins
136        PluginHandler.addDownloadSelection(downloadSelections);
137
138        // register all default download selections
139        for (int i = 0; i < downloadSelections.size(); i++) {
140            downloadSelections.get(i).addGui(this);
141        }
142
143        // allow to collapse the panes completely
144        downloadSourcesTab.setMinimumSize(new Dimension(0, 0));
145        tpDownloadAreaSelectors.setMinimumSize(new Dimension(0, 0));
146
147        dialogSplit = new JSplitPane(
148                JSplitPane.VERTICAL_SPLIT,
149                downloadSourcesTab,
150                tpDownloadAreaSelectors);
151
152        mainPanel.add(dialogSplit, GBC.eol().fill());
153
154        cbNewLayer = new JCheckBox(tr("Download as new layer"));
155        cbNewLayer.setToolTipText(tr("<html>Select to download data into a new data layer.<br>"
156                +"Unselect to download into the currently active data layer.</html>"));
157
158        cbStartup = new JCheckBox(tr("Open this dialog on startup"));
159        cbStartup.setToolTipText(
160                tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" +
161                        "You can open it manually from File menu or toolbar.</html>"));
162        cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected()));
163
164        cbZoomToDownloadedData = new JCheckBox(tr("Zoom to downloaded data"));
165        cbZoomToDownloadedData.setToolTipText(tr("Select to zoom to entire newly downloaded data."));
166
167        mainPanel.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5));
168        mainPanel.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
169        mainPanel.add(cbZoomToDownloadedData, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
170
171        ExpertToggleAction.addVisibilitySwitcher(cbZoomToDownloadedData);
172
173        if (!ExpertToggleAction.isExpert()) {
174            JLabel infoLabel = new JLabel(
175                    tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom."));
176            mainPanel.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0));
177        }
178        return mainPanel;
179    }
180
181    /* This should not be necessary, but if not here, repaint is not always correct in SlippyMap! */
182    @Override
183    public void paint(Graphics g) {
184        tpDownloadAreaSelectors.getSelectedComponent().paint(g);
185        super.paint(g);
186    }
187
188    /**
189     * Builds the button pane of the dialog.
190     * @return The button panel of the dialog.
191     */
192    protected final JPanel buildButtonPanel() {
193        btnDownload = new JButton(new DownloadAction());
194        btnCancel = new JButton(new CancelAction());
195        btnHelp = new JButton(
196                new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString()));
197
198        JPanel pnl = new JPanel(new FlowLayout());
199
200        pnl.add(btnDownload);
201        pnl.add(btnCancel);
202        pnl.add(btnHelp);
203
204        InputMapUtils.enableEnter(btnDownload);
205        InputMapUtils.enableEnter(btnCancel);
206        InputMapUtils.addEscapeAction(getRootPane(), btnCancel.getAction());
207        InputMapUtils.enableEnter(btnHelp);
208
209        InputMapUtils.addEnterActionWhenAncestor(cbNewLayer, btnDownload.getAction());
210        InputMapUtils.addEnterActionWhenAncestor(cbStartup, btnDownload.getAction());
211        InputMapUtils.addEnterActionWhenAncestor(cbZoomToDownloadedData, btnDownload.getAction());
212
213        return pnl;
214    }
215
216    /**
217     * Constructs a new {@code DownloadDialog}.
218     * @param parent the parent component
219     */
220    public DownloadDialog(Component parent) {
221        this(parent, ht("/Action/Download"));
222    }
223
224    /**
225     * Constructs a new {@code DownloadDialog}.
226     * @param parent the parent component
227     * @param helpTopic the help topic to assign
228     */
229    public DownloadDialog(Component parent, String helpTopic) {
230        super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL);
231        HelpUtil.setHelpContext(getRootPane(), helpTopic);
232        getContentPane().setLayout(new BorderLayout());
233        getContentPane().add(buildMainPanel(), BorderLayout.CENTER);
234        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
235
236        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
237                KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), "checkClipboardContents");
238
239        getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
240            @Override
241            public void actionPerformed(ActionEvent e) {
242                String clip = ClipboardUtils.getClipboardStringContent();
243                if (clip == null) {
244                    return;
245                }
246                Bounds b = OsmUrlToBounds.parse(clip);
247                if (b != null) {
248                    boundingBoxChanged(new Bounds(b), null);
249                }
250            }
251        });
252        addWindowListener(new WindowEventHandler());
253        ExpertToggleAction.addExpertModeChangeListener(expertListener);
254        restoreSettings();
255    }
256
257    /**
258     * Distributes a "bounding box changed" from one DownloadSelection
259     * object to the others, so they may update or clear their input fields. Also informs
260     * download sources about the change, so they can react on it.
261     * @param b new current bounds
262     *
263     * @param eventSource - the DownloadSelection object that fired this notification.
264     */
265    public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) {
266        this.currentBounds = b;
267        for (DownloadSelection s : downloadSelections) {
268            if (s != eventSource) {
269                s.setDownloadArea(currentBounds);
270            }
271        }
272
273        for (Component ds : downloadSourcesTab.getComponents()) {
274            if (ds instanceof AbstractDownloadSourcePanel) {
275                ((AbstractDownloadSourcePanel) ds).boudingBoxChanged(b);
276            }
277        }
278    }
279
280    /**
281     * Starts download for the given bounding box
282     * @param b bounding box to download
283     */
284    public void startDownload(Bounds b) {
285        this.currentBounds = b;
286        startDownload();
287    }
288
289    /**
290     * Starts download.
291     */
292    public void startDownload() {
293        btnDownload.doClick();
294    }
295
296    /**
297     * Replies true if the user requires to download into a new layer
298     *
299     * @return true if the user requires to download into a new layer
300     */
301    public boolean isNewLayerRequired() {
302        return cbNewLayer.isSelected();
303    }
304
305    /**
306     * Replies true if the user requires to zoom to new downloaded data
307     *
308     * @return true if the user requires to zoom to new downloaded data
309     * @since 11658
310     */
311    public boolean isZoomToDownloadedDataRequired() {
312        return cbZoomToDownloadedData.isSelected();
313    }
314
315    /**
316     * Determines if the dialog autorun is enabled in preferences.
317     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise
318     */
319    public static boolean isAutorunEnabled() {
320        return DOWNLOAD_AUTORUN.get();
321    }
322
323    /**
324     * Adds a new download area selector to the download dialog
325     *
326     * @param selector the download are selector
327     * @param displayName the display name of the selector
328     */
329    public void addDownloadAreaSelector(JPanel selector, String displayName) {
330        tpDownloadAreaSelectors.add(displayName, selector);
331    }
332
333    /**
334     * Adds a new download source to the download dialog
335     *
336     * @param downloadSource The download source to be added.
337     * @param <T> The type of the download data.
338     */
339    public <T> void addDownloadSource(DownloadSource<T> downloadSource) {
340        if ((ExpertToggleAction.isExpert() && downloadSource.onlyExpert()) || !downloadSource.onlyExpert()) {
341            addNewDownloadSourceTab(downloadSource);
342        }
343    }
344
345    /**
346     * Refreshes the tile sources
347     * @since 6364
348     */
349    public final void refreshTileSources() {
350        if (slippyMapChooser != null) {
351            slippyMapChooser.refreshTileSources();
352        }
353    }
354
355    /**
356     * Remembers the current settings in the download dialog.
357     */
358    public void rememberSettings() {
359        DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex());
360        DOWNLOAD_SOURCE_TAB.put(downloadSourcesTab.getSelectedIndex());
361        DIALOG_SPLIT.put(dialogSplit.getDividerLocation());
362        DOWNLOAD_NEWLAYER.put(cbNewLayer.isSelected());
363        DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected());
364        if (currentBounds != null) {
365            Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";"));
366        }
367    }
368
369    /**
370     * Restores the previous settings in the download dialog.
371     */
372    public void restoreSettings() {
373        cbNewLayer.setSelected(DOWNLOAD_NEWLAYER.get());
374        cbStartup.setSelected(isAutorunEnabled());
375        cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get());
376        dialogSplit.setDividerLocation(DIALOG_SPLIT.get());
377
378        try {
379            tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get());
380        } catch (IndexOutOfBoundsException e) {
381            Main.trace(e);
382            tpDownloadAreaSelectors.setSelectedIndex(0);
383        }
384
385        try {
386            downloadSourcesTab.setSelectedIndex(DOWNLOAD_SOURCE_TAB.get());
387        } catch (IndexOutOfBoundsException e) {
388            Main.trace(e);
389            downloadSourcesTab.setSelectedIndex(0);
390        }
391
392        if (MainApplication.isDisplayingMapView()) {
393            MapView mv = MainApplication.getMap().mapView;
394            currentBounds = new Bounds(
395                    mv.getLatLon(0, mv.getHeight()),
396                    mv.getLatLon(mv.getWidth(), 0)
397            );
398            boundingBoxChanged(currentBounds, null);
399        } else {
400            Bounds bounds = getSavedDownloadBounds();
401            if (bounds != null) {
402                currentBounds = bounds;
403                boundingBoxChanged(currentBounds, null);
404            }
405        }
406    }
407
408    /**
409     * Returns the previously saved bounding box from preferences.
410     * @return The bounding box saved in preferences if any, {@code null} otherwise
411     * @since 6509
412     */
413    public static Bounds getSavedDownloadBounds() {
414        String value = Main.pref.get("osm-download.bounds");
415        if (!value.isEmpty()) {
416            try {
417                return new Bounds(value, ";");
418            } catch (IllegalArgumentException e) {
419                Logging.warn(e);
420            }
421        }
422        return null;
423    }
424
425    /**
426     * Automatically opens the download dialog, if autorun is enabled.
427     * @see #isAutorunEnabled
428     */
429    public static void autostartIfNeeded() {
430        if (isAutorunEnabled()) {
431            MainApplication.getMenu().download.actionPerformed(null);
432        }
433    }
434
435    /**
436     * Returns an {@link Optional} of the currently selected download area.
437     * @return An {@link Optional} of the currently selected download area.
438     * @since 12574 Return type changed to optional
439     */
440    public Optional<Bounds> getSelectedDownloadArea() {
441        return Optional.ofNullable(currentBounds);
442    }
443
444    @Override
445    public void setVisible(boolean visible) {
446        if (visible) {
447            new WindowGeometry(
448                    getClass().getName() + ".geometry",
449                    WindowGeometry.centerInWindow(
450                            getParent(),
451                            new Dimension(1000, 600)
452                    )
453            ).applySafe(this);
454        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
455            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
456        }
457        super.setVisible(visible);
458    }
459
460    /**
461     * Replies true if the dialog was canceled
462     *
463     * @return true if the dialog was canceled
464     */
465    public boolean isCanceled() {
466        return canceled;
467    }
468
469    /**
470     * Gets the global settings of the download dialog.
471     * @return The {@link DownloadSettings} object that describes the current state of
472     * the download dialog.
473     */
474    public DownloadSettings getDownloadSettings() {
475        return new DownloadSettings(isNewLayerRequired(), isZoomToDownloadedDataRequired());
476    }
477
478    protected void setCanceled(boolean canceled) {
479        this.canceled = canceled;
480    }
481
482    /**
483     * Returns position of the download source in the tabbed pane.
484     * @param downloadSource The download source.
485     * @return The index of the download source, or -1 if it not in the pane.
486     */
487    protected int getDownloadSourceIndex(DownloadSource downloadSource) {
488        return Arrays.stream(downloadSourcesTab.getComponents())
489                .filter(it -> it instanceof AbstractDownloadSourcePanel)
490                .map(it -> (AbstractDownloadSourcePanel) it)
491                .filter(it -> it.getDownloadSource().equals(downloadSource))
492                .findAny()
493                .map(downloadSourcesTab::indexOfComponent)
494                .orElse(-1);
495    }
496
497    /**
498     * Adds the download source to the download sources tab.
499     * @param downloadSource The download source to be added.
500     * @param <T> The type of the download data.
501     */
502    private <T> void addNewDownloadSourceTab(DownloadSource<T> downloadSource) {
503        AbstractDownloadSourcePanel<T> panel = downloadSource.createPanel();
504        downloadSourcesTab.add(panel, downloadSource.getLabel());
505        Icon icon = panel.getIcon();
506        if (icon != null) {
507            int idx = getDownloadSourceIndex(downloadSource);
508            downloadSourcesTab.setIconAt(
509                    idx != -1 ? idx : downloadSourcesTab.getTabCount() - 1,
510                    icon);
511        }
512    }
513
514    /**
515     * Creates listener that removes/adds download sources from/to {@code downloadSourcesTab}
516     * depending on the current mode.
517     * @return The expert mode listener.
518     */
519    private ExpertToggleAction.ExpertModeChangeListener getExpertModeListenerForDownloadSources() {
520        return isExpert -> {
521            if (isExpert) {
522                downloadSources.stream()
523                        .filter(DownloadSource::onlyExpert)
524                        .filter(it -> getDownloadSourceIndex(it) == -1)
525                        .forEach(this::addNewDownloadSourceTab);
526            } else {
527                IntStream.range(0, downloadSourcesTab.getTabCount())
528                        .mapToObj(downloadSourcesTab::getComponentAt)
529                        .filter(it -> it instanceof AbstractDownloadSourcePanel)
530                        .map(it -> (AbstractDownloadSourcePanel) it)
531                        .filter(it -> it.getDownloadSource().onlyExpert())
532                        .forEach(downloadSourcesTab::remove);
533            }
534        };
535    }
536
537    /**
538     * Action that is executed when the cancel button is pressed.
539     */
540    class CancelAction extends AbstractAction {
541        CancelAction() {
542            putValue(NAME, tr("Cancel"));
543            new ImageProvider("cancel").getResource().attachImageIcon(this);
544            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
545        }
546
547        public void run() {
548            setCanceled(true);
549            setVisible(false);
550        }
551
552        @Override
553        public void actionPerformed(ActionEvent e) {
554            AbstractDownloadSourcePanel pnl = (AbstractDownloadSourcePanel) downloadSourcesTab.getSelectedComponent();
555            run();
556            pnl.checkCancel();
557        }
558    }
559
560    /**
561     * Action that is executed when the download button is pressed.
562     */
563    class DownloadAction extends AbstractAction {
564        DownloadAction() {
565            putValue(NAME, tr("Download"));
566            new ImageProvider("download").getResource().attachImageIcon(this);
567            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
568            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
569        }
570
571        public void run() {
572            Component panel = downloadSourcesTab.getSelectedComponent();
573            if (panel instanceof AbstractDownloadSourcePanel) {
574                AbstractDownloadSourcePanel pnl = (AbstractDownloadSourcePanel) panel;
575                DownloadSettings downloadSettings = getDownloadSettings();
576                if (pnl.checkDownload(currentBounds, downloadSettings)) {
577                    rememberSettings();
578                    setCanceled(false);
579                    setVisible(false);
580                    pnl.getDownloadSource().doDownload(currentBounds, pnl.getData(), downloadSettings);
581                }
582            }
583        }
584
585        @Override
586        public void actionPerformed(ActionEvent e) {
587            run();
588        }
589    }
590
591    class WindowEventHandler extends WindowAdapter {
592        @Override
593        public void windowClosing(WindowEvent e) {
594            new CancelAction().run();
595        }
596
597        @Override
598        public void windowActivated(WindowEvent e) {
599            btnDownload.requestFocusInWindow();
600        }
601    }
602}
Note: See TracBrowser for help on using the repository browser.