001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.BorderLayout;
008import java.awt.Color;
009import java.awt.Component;
010import java.awt.Dimension;
011import java.awt.FlowLayout;
012import java.awt.GridBagLayout;
013import java.awt.event.ActionEvent;
014import java.awt.event.ComponentAdapter;
015import java.awt.event.ComponentEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Optional;
021import java.util.stream.Collectors;
022import java.util.stream.IntStream;
023
024import javax.swing.AbstractAction;
025import javax.swing.BorderFactory;
026import javax.swing.Box;
027import javax.swing.Icon;
028import javax.swing.JButton;
029import javax.swing.JCheckBox;
030import javax.swing.JComponent;
031import javax.swing.JDialog;
032import javax.swing.JLabel;
033import javax.swing.JPanel;
034import javax.swing.JSplitPane;
035import javax.swing.JTabbedPane;
036import javax.swing.event.ChangeEvent;
037import javax.swing.event.ChangeListener;
038
039import org.openstreetmap.josm.actions.ExpertToggleAction;
040import org.openstreetmap.josm.data.Bounds;
041import org.openstreetmap.josm.data.coor.ILatLon;
042import org.openstreetmap.josm.data.coor.LatLon;
043import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
044import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat;
045import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat;
046import org.openstreetmap.josm.data.preferences.BooleanProperty;
047import org.openstreetmap.josm.data.preferences.IntegerProperty;
048import org.openstreetmap.josm.data.preferences.StringProperty;
049import org.openstreetmap.josm.gui.MainApplication;
050import org.openstreetmap.josm.gui.MapView;
051import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
052import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
053import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
054import org.openstreetmap.josm.gui.help.HelpUtil;
055import org.openstreetmap.josm.gui.layer.OsmDataLayer;
056import org.openstreetmap.josm.gui.util.GuiHelper;
057import org.openstreetmap.josm.gui.util.WindowGeometry;
058import org.openstreetmap.josm.gui.widgets.ImageLabel;
059import org.openstreetmap.josm.io.NetworkManager;
060import org.openstreetmap.josm.io.OnlineResource;
061import org.openstreetmap.josm.plugins.PluginHandler;
062import org.openstreetmap.josm.spi.preferences.Config;
063import org.openstreetmap.josm.tools.GBC;
064import org.openstreetmap.josm.tools.ImageProvider;
065import org.openstreetmap.josm.tools.InputMapUtils;
066import org.openstreetmap.josm.tools.JosmRuntimeException;
067import org.openstreetmap.josm.tools.ListenerList;
068import org.openstreetmap.josm.tools.Logging;
069import org.openstreetmap.josm.tools.OsmUrlToBounds;
070import org.openstreetmap.josm.tools.Shortcut;
071
072/**
073 * Dialog displayed to the user to download mapping data.
074 */
075public class DownloadDialog extends JDialog {
076
077    private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0);
078    private static final StringProperty DOWNLOAD_SOURCE_TAB = new StringProperty("download.source.tab", OSMDownloadSource.SIMPLE_NAME);
079    private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false);
080    private static final BooleanProperty DOWNLOAD_ZOOMTODATA = new BooleanProperty("download.zoomtodata", true);
081
082    /** the unique instance of the download dialog */
083    private static DownloadDialog instance;
084
085    /**
086     * Replies the unique instance of the download dialog
087     *
088     * @return the unique instance of the download dialog
089     */
090    public static synchronized DownloadDialog getInstance() {
091        if (instance == null) {
092            instance = new DownloadDialog(MainApplication.getMainFrame());
093        }
094        return instance;
095    }
096
097    private static final ListenerList<DownloadSourceListener> downloadSourcesListeners = ListenerList.create();
098    private static final List<DownloadSource<?>> downloadSources = new ArrayList<>();
099    static {
100        // add default download sources
101        addDownloadSource(new OSMDownloadSource());
102        addDownloadSource(new OverpassDownloadSource());
103    }
104
105    protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>();
106    protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane();
107    protected final DownloadSourceTabs downloadSourcesTab = new DownloadSourceTabs();
108
109    private final ImageLabel latText;
110    private final ImageLabel lonText;
111    private final ImageLabel bboxText;
112    {
113        final LatLon sample = new LatLon(90, 180);
114        final ICoordinateFormat sampleFormat = DMSCoordinateFormat.INSTANCE;
115        final Color background = new JPanel().getBackground();
116        latText = new ImageLabel("lat", null, sampleFormat.latToString(sample).length(), background);
117        lonText = new ImageLabel("lon", null, sampleFormat.lonToString(sample).length(), background);
118        bboxText = new ImageLabel("bbox", null, sampleFormat.toString(sample, "").length() * 2, background);
119    }
120
121    protected JCheckBox cbStartup;
122    protected JCheckBox cbZoomToDownloadedData;
123    protected SlippyMapChooser slippyMapChooser;
124    protected JPanel mainPanel;
125    protected DownloadDialogSplitPane dialogSplit;
126
127    /*
128     * Keep the reference globally to avoid having it garbage collected
129     */
130    protected final transient ExpertToggleAction.ExpertModeChangeListener expertListener =
131            getExpertModeListenerForDownloadSources();
132    protected transient Bounds currentBounds;
133    protected boolean canceled;
134
135    protected JButton btnDownload;
136    protected JButton btnDownloadNewLayer;
137    protected JButton btnCancel;
138    protected JButton btnHelp;
139
140    /**
141     * Builds the main panel of the dialog.
142     * @return The panel of the dialog.
143     */
144    protected final JPanel buildMainPanel() {
145        mainPanel = new JPanel(new GridBagLayout());
146
147        // must be created before hook
148        slippyMapChooser = new SlippyMapChooser();
149
150        // predefined download selections
151        downloadSelections.add(slippyMapChooser);
152        downloadSelections.add(new BookmarkSelection());
153        downloadSelections.add(new BoundingBoxSelection());
154        downloadSelections.add(new PlaceSelection());
155        downloadSelections.add(new TileSelection());
156
157        // add selections from plugins
158        PluginHandler.addDownloadSelection(downloadSelections);
159
160        // register all default download selections
161        for (DownloadSelection s : downloadSelections) {
162            s.addGui(this);
163        }
164
165        // allow to collapse the panes, but reserve some space for tabs
166        downloadSourcesTab.setMinimumSize(new Dimension(0, 25));
167        tpDownloadAreaSelectors.setMinimumSize(new Dimension(0, 0));
168
169        dialogSplit = new DownloadDialogSplitPane(
170                downloadSourcesTab,
171                tpDownloadAreaSelectors);
172
173        ChangeListener tabChangedListener = getDownloadSourceTabChangeListener();
174        tabChangedListener.stateChanged(new ChangeEvent(downloadSourcesTab));
175        downloadSourcesTab.addChangeListener(tabChangedListener);
176
177        mainPanel.add(dialogSplit, GBC.eol().fill());
178
179        JPanel statusBarPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
180        statusBarPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY));
181        statusBarPanel.add(latText);
182        statusBarPanel.add(lonText);
183        statusBarPanel.add(bboxText);
184        mainPanel.add(statusBarPanel, GBC.eol().fill(GBC.HORIZONTAL));
185        ExpertToggleAction.addVisibilitySwitcher(statusBarPanel);
186
187        cbStartup = new JCheckBox(tr("Open this dialog on startup"));
188        cbStartup.setToolTipText(
189                tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" +
190                        "You can open it manually from File menu or toolbar.</html>"));
191        cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected()));
192
193        JLabel iconZoomToDownloadedData = new JLabel(ImageProvider.get("dialogs/autoscale/download", ImageProvider.ImageSizes.SMALLICON));
194        cbZoomToDownloadedData = new JCheckBox(tr("Zoom to downloaded data"));
195        cbZoomToDownloadedData.setToolTipText(tr("Select to zoom to entire newly downloaded data."));
196
197        JPanel checkboxPanel = new JPanel(new FlowLayout());
198        checkboxPanel.add(cbStartup);
199        checkboxPanel.add(Box.createHorizontalStrut(6));
200        checkboxPanel.add(iconZoomToDownloadedData);
201        checkboxPanel.add(cbZoomToDownloadedData);
202        mainPanel.add(checkboxPanel, GBC.eol());
203
204        ExpertToggleAction.addVisibilitySwitcher(cbZoomToDownloadedData);
205        ExpertToggleAction.addVisibilitySwitcher(iconZoomToDownloadedData);
206
207        JLabel infoLabel = new JLabel(
208                tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom."));
209        mainPanel.add(infoLabel, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 0));
210
211        ExpertToggleAction.addExpertModeChangeListener(isExpert -> infoLabel.setVisible(!isExpert), true);
212
213        return mainPanel;
214    }
215
216    /**
217     * Builds the button pane of the dialog.
218     * @return The button panel of the dialog.
219     */
220    protected final JPanel buildButtonPanel() {
221        btnDownload = new JButton(new DownloadAction(false));
222        btnDownloadNewLayer = new JButton(new DownloadAction(true));
223        btnCancel = new JButton(new CancelAction());
224        btnHelp = new JButton(
225                new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString()));
226
227        JPanel pnl = new JPanel(new FlowLayout());
228
229        pnl.add(btnDownload);
230        pnl.add(btnDownloadNewLayer);
231        pnl.add(btnCancel);
232        pnl.add(btnHelp);
233
234        InputMapUtils.enableEnter(btnDownload);
235        InputMapUtils.enableEnter(btnCancel);
236        InputMapUtils.addEscapeAction(getRootPane(), btnCancel.getAction());
237        InputMapUtils.enableEnter(btnHelp);
238
239        InputMapUtils.addEnterActionWhenAncestor(cbStartup, btnDownload.getAction());
240        InputMapUtils.addEnterActionWhenAncestor(cbZoomToDownloadedData, btnDownload.getAction());
241        InputMapUtils.addCtrlEnterAction(pnl, btnDownload.getAction());
242
243        return pnl;
244    }
245
246    /**
247     * Constructs a new {@code DownloadDialog}.
248     * @param parent the parent component
249     */
250    public DownloadDialog(Component parent) {
251        this(parent, ht("/Action/Download"));
252    }
253
254    /**
255     * Constructs a new {@code DownloadDialog}.
256     * @param parent the parent component
257     * @param helpTopic the help topic to assign
258     */
259    public DownloadDialog(Component parent, String helpTopic) {
260        super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL);
261        HelpUtil.setHelpContext(getRootPane(), helpTopic);
262        getContentPane().setLayout(new BorderLayout());
263        getContentPane().add(buildMainPanel(), BorderLayout.CENTER);
264        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
265
266        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
267                Shortcut.getPasteKeyStroke(), "checkClipboardContents");
268
269        getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
270            @Override
271            public void actionPerformed(ActionEvent e) {
272                String clip = ClipboardUtils.getClipboardStringContent();
273                if (clip == null) {
274                    return;
275                }
276                Bounds b = OsmUrlToBounds.parse(clip);
277                if (b != null) {
278                    boundingBoxChanged(new Bounds(b), null);
279                }
280            }
281        });
282        addWindowListener(new WindowEventHandler());
283        ExpertToggleAction.addExpertModeChangeListener(expertListener);
284        restoreSettings();
285
286        // if no bounding box is selected make sure it is still propagated.
287        if (currentBounds == null) {
288            boundingBoxChanged(null, null);
289        }
290    }
291
292    /**
293     * Distributes a "bounding box changed" from one DownloadSelection
294     * object to the others, so they may update or clear their input fields. Also informs
295     * download sources about the change, so they can react on it.
296     * @param b new current bounds
297     *
298     * @param eventSource - the DownloadSelection object that fired this notification.
299     */
300    public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) {
301        this.currentBounds = b;
302        for (DownloadSelection s : downloadSelections) {
303            if (s != eventSource) {
304                s.setDownloadArea(currentBounds);
305            }
306        }
307
308        for (AbstractDownloadSourcePanel<?> ds : downloadSourcesTab.getAllPanels()) {
309            ds.boundingBoxChanged(b);
310        }
311
312        bboxText.setText(b == null ? "" : String.join("  ",
313                CoordinateFormatManager.getDefaultFormat().toString(b.getMin(), " "),
314                CoordinateFormatManager.getDefaultFormat().toString(b.getMax(), " ")));
315    }
316
317    /**
318     * Updates the coordinates after moving the mouse cursor
319     * @param latLon the coordinates under the mouse cursor
320     */
321    public void mapCursorChanged(ILatLon latLon) {
322        latText.setText(CoordinateFormatManager.getDefaultFormat().latToString(latLon));
323        lonText.setText(CoordinateFormatManager.getDefaultFormat().lonToString(latLon));
324    }
325
326    /**
327     * Starts download for the given bounding box
328     * @param b bounding box to download
329     */
330    public void startDownload(Bounds b) {
331        this.currentBounds = b;
332        startDownload();
333    }
334
335    /**
336     * Starts download.
337     */
338    public void startDownload() {
339        btnDownload.doClick();
340    }
341
342    /**
343     * Replies true if the user requires to zoom to new downloaded data
344     *
345     * @return true if the user requires to zoom to new downloaded data
346     * @since 11658
347     */
348    public boolean isZoomToDownloadedDataRequired() {
349        return cbZoomToDownloadedData.isSelected();
350    }
351
352    /**
353     * Determines if the dialog autorun is enabled in preferences.
354     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise.
355     */
356    public static boolean isAutorunEnabled() {
357        return DOWNLOAD_AUTORUN.get();
358    }
359
360    /**
361     * Adds a new download area selector to the download dialog.
362     *
363     * @param selector the download are selector.
364     * @param displayName the display name of the selector.
365     */
366    public void addDownloadAreaSelector(JPanel selector, String displayName) {
367        tpDownloadAreaSelectors.add(displayName, selector);
368    }
369
370    /**
371     * Add a listener to get events from the DownloadSelection window
372     *
373     * @param selection The DownloadSelection object to send the Bounds to
374     * @return See {@link List#add}
375     * @since 16684
376     */
377    public boolean addDownloadAreaListener(DownloadSelection selection) {
378        return downloadSelections.add(selection);
379    }
380
381    /**
382     * Remove a listener that was getting events from the DownloadSelection window
383     *
384     * @param selection The DownloadSelection object to not send the Bounds to
385     * @return See {@link List#remove}
386     * @since 16684
387     */
388    public boolean removeDownloadAreaListener(DownloadSelection selection) {
389        return downloadSelections.remove(selection);
390    }
391
392    /**
393     * Adds a new download source to the download dialog if it is not added.
394     *
395     * @param downloadSource The download source to be added.
396     * @param <T> The type of the download data.
397     * @throws JosmRuntimeException If the download source is already added. Note, download sources are
398     * compared by their reference.
399     * @since 12878
400     */
401    public static <T> void addDownloadSource(DownloadSource<T> downloadSource) {
402        if (downloadSources.contains(downloadSource)) {
403            throw new JosmRuntimeException("The download source you are trying to add already exists.");
404        }
405
406        downloadSources.add(downloadSource);
407        downloadSourcesListeners.fireEvent(l -> l.downloadSourceAdded(downloadSource));
408    }
409
410    /**
411     * Remove a download source from the download dialog
412     *
413     * @param downloadSource The download source to be removed.
414     * @return see {@link List#remove}
415     * @since 15542
416     */
417    public static boolean removeDownloadSource(DownloadSource<?> downloadSource) {
418        if (downloadSources.contains(downloadSource)) {
419            return downloadSources.remove(downloadSource);
420        }
421        return false;
422    }
423
424    /**
425     * Refreshes the tile sources.
426     * @since 6364
427     */
428    public final void refreshTileSources() {
429        if (slippyMapChooser != null) {
430            slippyMapChooser.refreshTileSources();
431        }
432    }
433
434    /**
435     * Remembers the current settings in the download dialog.
436     */
437    public void rememberSettings() {
438        DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex());
439        downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::rememberSettings);
440        downloadSourcesTab.getSelectedPanel().ifPresent(panel -> DOWNLOAD_SOURCE_TAB.put(panel.getSimpleName()));
441        DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected());
442    }
443
444    /**
445     * Restores the previous settings in the download dialog.
446     */
447    public void restoreSettings() {
448        cbStartup.setSelected(isAutorunEnabled());
449        cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get());
450
451        try {
452            tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get());
453        } catch (IndexOutOfBoundsException e) {
454            Logging.trace(e);
455            tpDownloadAreaSelectors.setSelectedIndex(0);
456        }
457
458        downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::restoreSettings);
459        downloadSourcesTab.setSelected(DOWNLOAD_SOURCE_TAB.get());
460
461        if (MainApplication.isDisplayingMapView()) {
462            MapView mv = MainApplication.getMap().mapView;
463            currentBounds = new Bounds(
464                    mv.getLatLon(0, mv.getHeight()),
465                    mv.getLatLon(mv.getWidth(), 0)
466            );
467            boundingBoxChanged(currentBounds, null);
468        } else {
469            Bounds bounds = getSavedDownloadBounds();
470            if (bounds != null) {
471                currentBounds = bounds;
472                boundingBoxChanged(currentBounds, null);
473            }
474        }
475    }
476
477    /**
478     * Returns the previously saved bounding box from preferences.
479     * @return The bounding box saved in preferences if any, {@code null} otherwise.
480     * @since 6509
481     */
482    public static Bounds getSavedDownloadBounds() {
483        String value = Config.getPref().get("osm-download.bounds");
484        if (!value.isEmpty()) {
485            try {
486                return new Bounds(value, ";");
487            } catch (IllegalArgumentException e) {
488                Logging.warn(e);
489            }
490        }
491        return null;
492    }
493
494    /**
495     * Automatically opens the download dialog, if autorun is enabled.
496     * @see #isAutorunEnabled
497     */
498    public static void autostartIfNeeded() {
499        if (isAutorunEnabled()) {
500            MainApplication.getMenu().download.actionPerformed(null);
501        }
502    }
503
504    /**
505     * Returns an {@link Optional} of the currently selected download area.
506     * @return An {@link Optional} of the currently selected download area.
507     * @since 12574 Return type changed to optional
508     */
509    public Optional<Bounds> getSelectedDownloadArea() {
510        return Optional.ofNullable(currentBounds);
511    }
512
513    @Override
514    public void setVisible(boolean visible) {
515        if (visible) {
516            btnDownloadNewLayer.setEnabled(
517                    !MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).isEmpty());
518            new WindowGeometry(
519                    getClass().getName() + ".geometry",
520                    WindowGeometry.centerInWindow(
521                            getParent(),
522                            new Dimension(1000, 600)
523                    )
524            ).applySafe(this);
525        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
526            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
527        }
528        super.setVisible(visible);
529    }
530
531    /**
532     * Replies true if the dialog was canceled
533     *
534     * @return true if the dialog was canceled
535     */
536    public boolean isCanceled() {
537        return canceled;
538    }
539
540    /**
541     * Gets the global settings of the download dialog.
542     * @param newLayer The flag defining if a new layer must be created for the downloaded data.
543     * @return The {@link DownloadSettings} object that describes the current state of
544     * the download dialog.
545     */
546    public DownloadSettings getDownloadSettings(boolean newLayer) {
547        final Component areaSelector = tpDownloadAreaSelectors.getSelectedComponent();
548        final Bounds slippyMapBounds = areaSelector instanceof SlippyMapBBoxChooser
549                ? ((SlippyMapBBoxChooser) areaSelector).getVisibleMapArea()
550                : null;
551        return new DownloadSettings(currentBounds, slippyMapBounds, newLayer, isZoomToDownloadedDataRequired());
552    }
553
554    protected void setCanceled(boolean canceled) {
555        this.canceled = canceled;
556    }
557
558    /**
559     * Adds the download source to the download sources tab.
560     * @param downloadSource The download source to be added.
561     * @param <T> The type of the download data.
562     */
563    protected <T> void addNewDownloadSourceTab(DownloadSource<T> downloadSource) {
564        downloadSourcesTab.addPanel(downloadSource.createPanel(this));
565    }
566
567    /**
568     * Creates listener that removes/adds download sources from/to {@code downloadSourcesTab}
569     * depending on the current mode.
570     * @return The expert mode listener.
571     */
572    private ExpertToggleAction.ExpertModeChangeListener getExpertModeListenerForDownloadSources() {
573        return downloadSourcesTab::updateExpert;
574    }
575
576    /**
577     * Creates a listener that reacts on tab switches for {@code downloadSourcesTab} in order
578     * to adjust proper division of the dialog according to user saved preferences or minimal size
579     * of the panel.
580     * @return A listener to adjust dialog division.
581     */
582    private ChangeListener getDownloadSourceTabChangeListener() {
583        return ec -> downloadSourcesTab.getSelectedPanel().ifPresent(
584                panel -> dialogSplit.setPolicy(panel.getSizingPolicy()));
585    }
586
587    /**
588     * Action that is executed when the cancel button is pressed.
589     */
590    class CancelAction extends AbstractAction {
591        CancelAction() {
592            putValue(NAME, tr("Cancel"));
593            new ImageProvider("cancel").getResource().attachImageIcon(this);
594            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
595        }
596
597        /**
598         * Cancels the download
599         */
600        public void run() {
601            rememberSettings();
602            setCanceled(true);
603            setVisible(false);
604        }
605
606        @Override
607        public void actionPerformed(ActionEvent e) {
608            Optional<AbstractDownloadSourcePanel<?>> panel = downloadSourcesTab.getSelectedPanel();
609            run();
610            panel.ifPresent(AbstractDownloadSourcePanel::checkCancel);
611        }
612    }
613
614    /**
615     * Action that is executed when the download button is pressed.
616     */
617    class DownloadAction extends AbstractAction {
618        final boolean newLayer;
619        DownloadAction(boolean newLayer) {
620            this.newLayer = newLayer;
621            if (!newLayer) {
622                putValue(NAME, tr("Download"));
623                putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
624                new ImageProvider("download").getResource().attachImageIcon(this);
625            } else {
626                putValue(NAME, tr("Download as new layer"));
627                putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area into a new data layer"));
628                new ImageProvider("download_new_layer").getResource().attachImageIcon(this);
629            }
630            setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API));
631        }
632
633        /**
634         * Starts the download and closes the dialog, if all requirements for the current download source are met.
635         * Otherwise the download is not started and the dialog remains visible.
636         */
637        public void run() {
638            rememberSettings();
639            downloadSourcesTab.getSelectedPanel().ifPresent(panel -> {
640                DownloadSettings downloadSettings = getDownloadSettings(newLayer);
641                if (panel.checkDownload(downloadSettings)) {
642                    setCanceled(false);
643                    setVisible(false);
644                    panel.triggerDownload(downloadSettings);
645                }
646            });
647        }
648
649        @Override
650        public void actionPerformed(ActionEvent e) {
651            run();
652        }
653    }
654
655    class WindowEventHandler extends WindowAdapter {
656        @Override
657        public void windowClosing(WindowEvent e) {
658            new CancelAction().run();
659        }
660
661        @Override
662        public void windowActivated(WindowEvent e) {
663            btnDownload.requestFocusInWindow();
664        }
665    }
666
667    /**
668     * A special tabbed pane for {@link AbstractDownloadSourcePanel}s
669     * @author Michael Zangl
670     * @since 12706
671     */
672    private class DownloadSourceTabs extends JTabbedPane implements DownloadSourceListener {
673        private final List<AbstractDownloadSourcePanel<?>> allPanels = new ArrayList<>();
674
675        DownloadSourceTabs() {
676            downloadSources.forEach(this::downloadSourceAdded);
677            downloadSourcesListeners.addListener(this);
678        }
679
680        List<AbstractDownloadSourcePanel<?>> getAllPanels() {
681            return allPanels;
682        }
683
684        List<AbstractDownloadSourcePanel<?>> getVisiblePanels() {
685            return IntStream.range(0, getTabCount())
686                    .mapToObj(this::getComponentAt)
687                    .map(p -> (AbstractDownloadSourcePanel<?>) p)
688                    .collect(Collectors.toList());
689        }
690
691        void setSelected(String simpleName) {
692            getVisiblePanels().stream()
693                .filter(panel -> simpleName.equals(panel.getSimpleName()))
694                .findFirst()
695                .ifPresent(this::setSelectedComponent);
696        }
697
698        void updateExpert(boolean isExpert) {
699            updateTabs();
700        }
701
702        void addPanel(AbstractDownloadSourcePanel<?> panel) {
703            allPanels.add(panel);
704            updateTabs();
705        }
706
707        private void updateTabs() {
708            // Not the best performance, but we don't do it often
709            removeAll();
710
711            boolean isExpert = ExpertToggleAction.isExpert();
712            allPanels.stream()
713                .filter(panel -> isExpert || !panel.getDownloadSource().onlyExpert())
714                .forEach(panel -> addTab(panel.getDownloadSource().getLabel(), panel.getIcon(), panel));
715        }
716
717        Optional<AbstractDownloadSourcePanel<?>> getSelectedPanel() {
718            return Optional.ofNullable((AbstractDownloadSourcePanel<?>) getSelectedComponent());
719        }
720
721        @Override
722        public void insertTab(String title, Icon icon, Component component, String tip, int index) {
723            if (!(component instanceof AbstractDownloadSourcePanel)) {
724                throw new IllegalArgumentException("Can only add AbstractDownloadSourcePanels");
725            }
726            super.insertTab(title, icon, component, tip, index);
727        }
728
729        @Override
730        public void downloadSourceAdded(DownloadSource<?> source) {
731            addPanel(source.createPanel(DownloadDialog.this));
732        }
733    }
734
735    /**
736     * A special split pane that acts according to a {@link DownloadSourceSizingPolicy}
737     *
738     * It attempts to size the top tab content correctly.
739     *
740     * @author Michael Zangl
741     * @since 12705
742     */
743    private static class DownloadDialogSplitPane extends JSplitPane {
744        private DownloadSourceSizingPolicy policy;
745        private final JTabbedPane topComponent;
746        /**
747         * If the height was explicitly set by the user.
748         */
749        private boolean heightAdjustedExplicitly;
750
751        DownloadDialogSplitPane(JTabbedPane newTopComponent, Component newBottomComponent) {
752            super(VERTICAL_SPLIT, newTopComponent, newBottomComponent);
753            this.topComponent = newTopComponent;
754
755            addComponentListener(new ComponentAdapter() {
756                @Override
757                public void componentResized(ComponentEvent e) {
758                    // doLayout is called automatically when the component size decreases
759                    // This seems to be the only way to call doLayout when the component size increases
760                    // We need this since we sometimes want to increase the top component size.
761                    revalidate();
762                }
763            });
764
765            addPropertyChangeListener(DIVIDER_LOCATION_PROPERTY, e -> heightAdjustedExplicitly = true);
766        }
767
768        public void setPolicy(DownloadSourceSizingPolicy policy) {
769            this.policy = policy;
770
771            super.setDividerLocation(policy.getComponentHeight() + computeOffset());
772            setDividerSize(policy.isHeightAdjustable() ? 10 : 0);
773            setEnabled(policy.isHeightAdjustable());
774        }
775
776        @Override
777        public void doLayout() {
778            // We need to force this height before the layout manager is run.
779            // We cannot do this in the setDividerLocation, since the offset cannot be computed there.
780            int offset = computeOffset();
781            if (policy.isHeightAdjustable() && heightAdjustedExplicitly) {
782                policy.storeHeight(Math.max(getDividerLocation() - offset, 0));
783            }
784            // At least 30 pixel for map, if we have enough space
785            int maxValidDividerLocation = getHeight() > 150 ? getHeight() - 40 : getHeight();
786
787            super.setDividerLocation(Math.min(policy.getComponentHeight() + offset, maxValidDividerLocation));
788            super.doLayout();
789            // Order is important (set this after setDividerLocation/doLayout called the listener)
790            this.heightAdjustedExplicitly = false;
791        }
792
793        /**
794         * @return The difference between the content height and the divider location
795         */
796        private int computeOffset() {
797            Component selectedComponent = topComponent.getSelectedComponent();
798            return topComponent.getHeight() - (selectedComponent == null ? 0 : selectedComponent.getHeight());
799        }
800    }
801}