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.Font;
013import java.awt.Graphics;
014import java.awt.GridBagLayout;
015import java.awt.event.ActionEvent;
016import java.awt.event.InputEvent;
017import java.awt.event.KeyEvent;
018import java.awt.event.WindowAdapter;
019import java.awt.event.WindowEvent;
020import java.util.ArrayList;
021import java.util.List;
022
023import javax.swing.AbstractAction;
024import javax.swing.JButton;
025import javax.swing.JCheckBox;
026import javax.swing.JComponent;
027import javax.swing.JDialog;
028import javax.swing.JLabel;
029import javax.swing.JOptionPane;
030import javax.swing.JPanel;
031import javax.swing.JTabbedPane;
032import javax.swing.KeyStroke;
033import javax.swing.event.ChangeListener;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.actions.ExpertToggleAction;
037import org.openstreetmap.josm.data.Bounds;
038import org.openstreetmap.josm.data.preferences.BooleanProperty;
039import org.openstreetmap.josm.data.preferences.IntegerProperty;
040import org.openstreetmap.josm.gui.MapView;
041import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
042import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
043import org.openstreetmap.josm.gui.help.HelpUtil;
044import org.openstreetmap.josm.gui.util.GuiHelper;
045import org.openstreetmap.josm.io.OnlineResource;
046import org.openstreetmap.josm.plugins.PluginHandler;
047import org.openstreetmap.josm.tools.GBC;
048import org.openstreetmap.josm.tools.ImageProvider;
049import org.openstreetmap.josm.tools.InputMapUtils;
050import org.openstreetmap.josm.tools.OsmUrlToBounds;
051import org.openstreetmap.josm.tools.Utils;
052import org.openstreetmap.josm.tools.WindowGeometry;
053
054/**
055 * Dialog displayed to download OSM and/or GPS data from OSM server.
056 */
057public class DownloadDialog extends JDialog {
058    private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0);
059
060    private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false);
061    private static final BooleanProperty DOWNLOAD_OSM = new BooleanProperty("download.osm", true);
062    private static final BooleanProperty DOWNLOAD_GPS = new BooleanProperty("download.gps", false);
063    private static final BooleanProperty DOWNLOAD_NOTES = new BooleanProperty("download.notes", false);
064    private static final BooleanProperty DOWNLOAD_NEWLAYER = new BooleanProperty("download.newlayer", false);
065
066    /** the unique instance of the download dialog */
067    private static DownloadDialog instance;
068
069    /**
070     * Replies the unique instance of the download dialog
071     *
072     * @return the unique instance of the download dialog
073     */
074    public static synchronized DownloadDialog getInstance() {
075        if (instance == null) {
076            instance = new DownloadDialog(Main.parent);
077        }
078        return instance;
079    }
080
081    protected SlippyMapChooser slippyMapChooser;
082    protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>();
083    protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane();
084    protected JCheckBox cbNewLayer;
085    protected JCheckBox cbStartup;
086    protected final JLabel sizeCheck = new JLabel();
087    protected transient Bounds currentBounds;
088    protected boolean canceled;
089
090    protected JCheckBox cbDownloadOsmData;
091    protected JCheckBox cbDownloadGpxData;
092    protected JCheckBox cbDownloadNotes;
093    /** the download action and button */
094    private final DownloadAction actDownload = new DownloadAction();
095    protected final JButton btnDownload = new JButton(actDownload);
096
097    protected final JPanel buildMainPanel() {
098        JPanel pnl = new JPanel(new GridBagLayout());
099
100        // size check depends on selected data source
101        final ChangeListener checkboxChangeListener = e -> updateSizeCheck();
102
103        // adding the download tasks
104        pnl.add(new JLabel(tr("Data Sources and Types:")), GBC.std().insets(5, 5, 1, 5));
105        cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
106        cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
107        cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
108        pnl.add(cbDownloadOsmData, GBC.std().insets(1, 5, 1, 5));
109        cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
110        cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
111        cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
112        pnl.add(cbDownloadGpxData, GBC.std().insets(5, 5, 1, 5));
113        cbDownloadNotes = new JCheckBox(tr("Notes"));
114        cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
115        cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
116        pnl.add(cbDownloadNotes, GBC.eol().insets(50, 5, 1, 5));
117
118        // must be created before hook
119        slippyMapChooser = new SlippyMapChooser();
120
121        // hook for subclasses
122        buildMainPanelAboveDownloadSelections(pnl);
123
124        // predefined download selections
125        downloadSelections.add(slippyMapChooser);
126        downloadSelections.add(new BookmarkSelection());
127        downloadSelections.add(new BoundingBoxSelection());
128        downloadSelections.add(new PlaceSelection());
129        downloadSelections.add(new TileSelection());
130
131        // add selections from plugins
132        PluginHandler.addDownloadSelection(downloadSelections);
133
134        // now everybody may add their tab to the tabbed pane
135        // (not done right away to allow plugins to remove one of
136        // the default selectors!)
137        for (DownloadSelection s : downloadSelections) {
138            s.addGui(this);
139        }
140
141        pnl.add(tpDownloadAreaSelectors, GBC.eol().fill());
142
143        try {
144            tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get());
145        } catch (IndexOutOfBoundsException ex) {
146            Main.trace(ex);
147            DOWNLOAD_TAB.put(0);
148        }
149
150        Font labelFont = sizeCheck.getFont();
151        sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize()));
152
153        cbNewLayer = new JCheckBox(tr("Download as new layer"));
154        cbNewLayer.setToolTipText(tr("<html>Select to download data into a new data layer.<br>"
155                +"Unselect to download into the currently active data layer.</html>"));
156
157        cbStartup = new JCheckBox(tr("Open this dialog on startup"));
158        cbStartup.setToolTipText(
159                tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" +
160                        "You can open it manually from File menu or toolbar.</html>"));
161        cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected()));
162
163        pnl.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5));
164        pnl.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
165
166        pnl.add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
167
168        if (!ExpertToggleAction.isExpert()) {
169            JLabel infoLabel = new JLabel(
170                    tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom."));
171            pnl.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0));
172        }
173        return pnl;
174    }
175
176    /* This should not be necessary, but if not here, repaint is not always correct in SlippyMap! */
177    @Override
178    public void paint(Graphics g) {
179        tpDownloadAreaSelectors.getSelectedComponent().paint(g);
180        super.paint(g);
181    }
182
183    protected final JPanel buildButtonPanel() {
184        JPanel pnl = new JPanel(new FlowLayout());
185
186        // -- download button
187        pnl.add(btnDownload);
188        InputMapUtils.enableEnter(btnDownload);
189
190        InputMapUtils.addEnterActionWhenAncestor(cbDownloadGpxData, actDownload);
191        InputMapUtils.addEnterActionWhenAncestor(cbDownloadOsmData, actDownload);
192        InputMapUtils.addEnterActionWhenAncestor(cbDownloadNotes, actDownload);
193        InputMapUtils.addEnterActionWhenAncestor(cbNewLayer, actDownload);
194
195        // -- cancel button
196        JButton btnCancel;
197        CancelAction actCancel = new CancelAction();
198        btnCancel = new JButton(actCancel);
199        pnl.add(btnCancel);
200        InputMapUtils.enableEnter(btnCancel);
201
202        // -- cancel on ESC
203        InputMapUtils.addEscapeAction(getRootPane(), actCancel);
204
205        // -- help button
206        JButton btnHelp = new JButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString()));
207        pnl.add(btnHelp);
208        InputMapUtils.enableEnter(btnHelp);
209
210        return pnl;
211    }
212
213    /**
214     * Constructs a new {@code DownloadDialog}.
215     * @param parent the parent component
216     */
217    public DownloadDialog(Component parent) {
218        this(parent, ht("/Action/Download"));
219    }
220
221    /**
222     * Constructs a new {@code DownloadDialog}.
223     * @param parent the parent component
224     * @param helpTopic the help topic to assign
225     */
226    public DownloadDialog(Component parent, String helpTopic) {
227        super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL);
228        HelpUtil.setHelpContext(getRootPane(), helpTopic);
229        getContentPane().setLayout(new BorderLayout());
230        getContentPane().add(buildMainPanel(), BorderLayout.CENTER);
231        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
232
233        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
234                KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "checkClipboardContents");
235
236        getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
237            @Override
238            public void actionPerformed(ActionEvent e) {
239                String clip = ClipboardUtils.getClipboardStringContent();
240                if (clip == null) {
241                    return;
242                }
243                Bounds b = OsmUrlToBounds.parse(clip);
244                if (b != null) {
245                    boundingBoxChanged(new Bounds(b), null);
246                }
247            }
248        });
249        addWindowListener(new WindowEventHandler());
250        restoreSettings();
251    }
252
253    protected void updateSizeCheck() {
254        boolean isAreaTooLarge = false;
255        if (currentBounds == null) {
256            sizeCheck.setText(tr("No area selected yet"));
257            sizeCheck.setForeground(Color.darkGray);
258        } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
259            // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
260            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area-notes", 25);
261        } else {
262            // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
263            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area", 0.25);
264        }
265        displaySizeCheckResult(isAreaTooLarge);
266    }
267
268    protected void displaySizeCheckResult(boolean isAreaTooLarge) {
269        if (isAreaTooLarge) {
270            sizeCheck.setText(tr("Download area too large; will probably be rejected by server"));
271            sizeCheck.setForeground(Color.red);
272        } else {
273            sizeCheck.setText(tr("Download area ok, size probably acceptable to server"));
274            sizeCheck.setForeground(Color.darkGray);
275        }
276    }
277
278    /**
279     * Distributes a "bounding box changed" from one DownloadSelection
280     * object to the others, so they may update or clear their input fields.
281     * @param b new current bounds
282     *
283     * @param eventSource - the DownloadSelection object that fired this notification.
284     */
285    public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) {
286        this.currentBounds = b;
287        for (DownloadSelection s : downloadSelections) {
288            if (s != eventSource) {
289                s.setDownloadArea(currentBounds);
290            }
291        }
292        updateSizeCheck();
293    }
294
295    /**
296     * Starts download for the given bounding box
297     * @param b bounding box to download
298     */
299    public void startDownload(Bounds b) {
300        this.currentBounds = b;
301        actDownload.run();
302    }
303
304    /**
305     * Replies true if the user selected to download OSM data
306     *
307     * @return true if the user selected to download OSM data
308     */
309    public boolean isDownloadOsmData() {
310        return cbDownloadOsmData.isSelected();
311    }
312
313    /**
314     * Replies true if the user selected to download GPX data
315     *
316     * @return true if the user selected to download GPX data
317     */
318    public boolean isDownloadGpxData() {
319        return cbDownloadGpxData.isSelected();
320    }
321
322    /**
323     * Replies true if user selected to download notes
324     *
325     * @return true if user selected to download notes
326     */
327    public boolean isDownloadNotes() {
328        return cbDownloadNotes.isSelected();
329    }
330
331    /**
332     * Replies true if the user requires to download into a new layer
333     *
334     * @return true if the user requires to download into a new layer
335     */
336    public boolean isNewLayerRequired() {
337        return cbNewLayer.isSelected();
338    }
339
340    /**
341     * Adds a new download area selector to the download dialog
342     *
343     * @param selector the download are selector
344     * @param displayName the display name of the selector
345     */
346    public void addDownloadAreaSelector(JPanel selector, String displayName) {
347        tpDownloadAreaSelectors.add(displayName, selector);
348    }
349
350    /**
351     * Refreshes the tile sources
352     * @since 6364
353     */
354    public final void refreshTileSources() {
355        if (slippyMapChooser != null) {
356            slippyMapChooser.refreshTileSources();
357        }
358    }
359
360    /**
361     * Remembers the current settings in the download dialog.
362     */
363    public void rememberSettings() {
364        DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex());
365        DOWNLOAD_OSM.put(cbDownloadOsmData.isSelected());
366        DOWNLOAD_GPS.put(cbDownloadGpxData.isSelected());
367        DOWNLOAD_NOTES.put(cbDownloadNotes.isSelected());
368        DOWNLOAD_NEWLAYER.put(cbNewLayer.isSelected());
369        if (currentBounds != null) {
370            Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";"));
371        }
372    }
373
374    /**
375     * Restores the previous settings in the download dialog.
376     */
377    public void restoreSettings() {
378        cbDownloadOsmData.setSelected(DOWNLOAD_OSM.get());
379        cbDownloadGpxData.setSelected(DOWNLOAD_GPS.get());
380        cbDownloadNotes.setSelected(DOWNLOAD_NOTES.get());
381        cbNewLayer.setSelected(DOWNLOAD_NEWLAYER.get());
382        cbStartup.setSelected(isAutorunEnabled());
383        int idx = Utils.clamp(DOWNLOAD_TAB.get(), 0, tpDownloadAreaSelectors.getTabCount() - 1);
384        tpDownloadAreaSelectors.setSelectedIndex(idx);
385
386        if (Main.isDisplayingMapView()) {
387            MapView mv = Main.map.mapView;
388            currentBounds = new Bounds(
389                    mv.getLatLon(0, mv.getHeight()),
390                    mv.getLatLon(mv.getWidth(), 0)
391            );
392            boundingBoxChanged(currentBounds, null);
393        } else {
394            Bounds bounds = getSavedDownloadBounds();
395            if (bounds != null) {
396                currentBounds = bounds;
397                boundingBoxChanged(currentBounds, null);
398            }
399        }
400    }
401
402    /**
403     * Returns the previously saved bounding box from preferences.
404     * @return The bounding box saved in preferences if any, {@code null} otherwise
405     * @since 6509
406     */
407    public static Bounds getSavedDownloadBounds() {
408        String value = Main.pref.get("osm-download.bounds");
409        if (!value.isEmpty()) {
410            try {
411                return new Bounds(value, ";");
412            } catch (IllegalArgumentException e) {
413                Main.warn(e);
414            }
415        }
416        return null;
417    }
418
419    /**
420     * Determines if the dialog autorun is enabled in preferences.
421     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise
422     */
423    public static boolean isAutorunEnabled() {
424        return DOWNLOAD_AUTORUN.get();
425    }
426
427    /**
428     * Automatically opens the download dialog, if autorun is enabled.
429     * @see #isAutorunEnabled
430     */
431    public static void autostartIfNeeded() {
432        if (isAutorunEnabled()) {
433            Main.main.menu.download.actionPerformed(null);
434        }
435    }
436
437    /**
438     * Replies the currently selected download area.
439     * @return the currently selected download area. May be {@code null}, if no download area is selected yet.
440     */
441    public Bounds getSelectedDownloadArea() {
442        return currentBounds;
443    }
444
445    @Override
446    public void setVisible(boolean visible) {
447        if (visible) {
448            new WindowGeometry(
449                    getClass().getName() + ".geometry",
450                    WindowGeometry.centerInWindow(
451                            getParent(),
452                            new Dimension(1000, 600)
453                    )
454            ).applySafe(this);
455        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
456            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
457        }
458        super.setVisible(visible);
459    }
460
461    /**
462     * Replies true if the dialog was canceled
463     *
464     * @return true if the dialog was canceled
465     */
466    public boolean isCanceled() {
467        return canceled;
468    }
469
470    protected void setCanceled(boolean canceled) {
471        this.canceled = canceled;
472    }
473
474    protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
475        // Do nothing
476    }
477
478    class CancelAction extends AbstractAction {
479        CancelAction() {
480            putValue(NAME, tr("Cancel"));
481            new ImageProvider("cancel").getResource().attachImageIcon(this);
482            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
483        }
484
485        public void run() {
486            setCanceled(true);
487            setVisible(false);
488        }
489
490        @Override
491        public void actionPerformed(ActionEvent e) {
492            run();
493        }
494    }
495
496    class DownloadAction extends AbstractAction {
497        DownloadAction() {
498            putValue(NAME, tr("Download"));
499            new ImageProvider("download").getResource().attachImageIcon(this);
500            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
501            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
502        }
503
504        public void run() {
505            if (currentBounds == null) {
506                JOptionPane.showMessageDialog(
507                        DownloadDialog.this,
508                        tr("Please select a download area first."),
509                        tr("Error"),
510                        JOptionPane.ERROR_MESSAGE
511                );
512                return;
513            }
514            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
515                JOptionPane.showMessageDialog(
516                        DownloadDialog.this,
517                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
518                                + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
519                                cbDownloadOsmData.getText(),
520                                cbDownloadGpxData.getText(),
521                                cbDownloadNotes.getText()
522                        ),
523                        tr("Error"),
524                        JOptionPane.ERROR_MESSAGE
525                );
526                return;
527            }
528            setCanceled(false);
529            setVisible(false);
530        }
531
532        @Override
533        public void actionPerformed(ActionEvent e) {
534            run();
535        }
536    }
537
538    class WindowEventHandler extends WindowAdapter {
539        @Override
540        public void windowClosing(WindowEvent e) {
541            new CancelAction().run();
542        }
543
544        @Override
545        public void windowActivated(WindowEvent e) {
546            btnDownload.requestFocusInWindow();
547        }
548    }
549}