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.ActionListener;
017import java.awt.event.InputEvent;
018import java.awt.event.KeyEvent;
019import java.awt.event.WindowAdapter;
020import java.awt.event.WindowEvent;
021import java.util.ArrayList;
022import java.util.List;
023
024import javax.swing.AbstractAction;
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.ChangeEvent;
034import javax.swing.event.ChangeListener;
035
036import org.openstreetmap.josm.Main;
037import org.openstreetmap.josm.actions.ExpertToggleAction;
038import org.openstreetmap.josm.data.Bounds;
039import org.openstreetmap.josm.gui.MapView;
040import org.openstreetmap.josm.gui.SideButton;
041import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
042import org.openstreetmap.josm.gui.help.HelpUtil;
043import org.openstreetmap.josm.io.OnlineResource;
044import org.openstreetmap.josm.plugins.PluginHandler;
045import org.openstreetmap.josm.tools.GBC;
046import org.openstreetmap.josm.tools.ImageProvider;
047import org.openstreetmap.josm.tools.InputMapUtils;
048import org.openstreetmap.josm.tools.OsmUrlToBounds;
049import org.openstreetmap.josm.tools.Utils;
050import org.openstreetmap.josm.tools.WindowGeometry;
051
052/**
053 * Dialog displayed to download OSM and/or GPS data from OSM server.
054 */
055public class DownloadDialog extends JDialog  {
056    /** the unique instance of the download dialog */
057    private static DownloadDialog instance;
058
059    /**
060     * Replies the unique instance of the download dialog
061     *
062     * @return the unique instance of the download dialog
063     */
064    public static synchronized DownloadDialog getInstance() {
065        if (instance == null) {
066            instance = new DownloadDialog(Main.parent);
067        }
068        return instance;
069    }
070
071    protected SlippyMapChooser slippyMapChooser;
072    protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>();
073    protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane();
074    protected JCheckBox cbNewLayer;
075    protected JCheckBox cbStartup;
076    protected final JLabel sizeCheck = new JLabel();
077    protected transient Bounds currentBounds;
078    protected boolean canceled;
079
080    protected JCheckBox cbDownloadOsmData;
081    protected JCheckBox cbDownloadGpxData;
082    protected JCheckBox cbDownloadNotes;
083    /** the download action and button */
084    private DownloadAction actDownload;
085    protected SideButton btnDownload;
086
087    private void makeCheckBoxRespondToEnter(JCheckBox cb) {
088        cb.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "doDownload");
089        cb.getActionMap().put("doDownload", actDownload);
090    }
091
092    protected final JPanel buildMainPanel() {
093        JPanel pnl = new JPanel(new GridBagLayout());
094
095        final ChangeListener checkboxChangeListener = new ChangeListener() {
096            @Override
097            public void stateChanged(ChangeEvent e) {
098                // size check depends on selected data source
099                updateSizeCheck();
100            }
101        };
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(Main.pref.getInteger("download.tab", 0));
145        } catch (Exception ex) {
146            Main.pref.putInteger("download.tab", 0);
147        }
148
149        Font labelFont = sizeCheck.getFont();
150        sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize()));
151
152        cbNewLayer = new JCheckBox(tr("Download as new layer"));
153        cbNewLayer.setToolTipText(tr("<html>Select to download data into a new data layer.<br>"
154                +"Unselect to download into the currently active data layer.</html>"));
155
156        cbStartup = new JCheckBox(tr("Open this dialog on startup"));
157        cbStartup.setToolTipText(
158                tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" +
159                        "You can open it manually from File menu or toolbar.</html>"));
160        cbStartup.addActionListener(new ActionListener() {
161            @Override
162            public void actionPerformed(ActionEvent e) {
163                 Main.pref.put("download.autorun", cbStartup.isSelected());
164            }
165        });
166
167        pnl.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5));
168        pnl.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
169
170        pnl.add(sizeCheck,  GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
171
172        if (!ExpertToggleAction.isExpert()) {
173            JLabel infoLabel  = new JLabel(
174                    tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom."));
175            pnl.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0));
176        }
177        return pnl;
178    }
179
180    /* This should not be necessary, but if not here, repaint is not always correct in SlippyMap! */
181    @Override
182    public void paint(Graphics g) {
183        tpDownloadAreaSelectors.getSelectedComponent().paint(g);
184        super.paint(g);
185    }
186
187    protected final JPanel buildButtonPanel() {
188        JPanel pnl = new JPanel(new FlowLayout());
189
190        // -- download button
191        pnl.add(btnDownload = new SideButton(actDownload = new DownloadAction()));
192        InputMapUtils.enableEnter(btnDownload);
193
194        makeCheckBoxRespondToEnter(cbDownloadGpxData);
195        makeCheckBoxRespondToEnter(cbDownloadOsmData);
196        makeCheckBoxRespondToEnter(cbDownloadNotes);
197        makeCheckBoxRespondToEnter(cbNewLayer);
198
199        // -- cancel button
200        SideButton btnCancel;
201        CancelAction actCancel = new CancelAction();
202        pnl.add(btnCancel = new SideButton(actCancel));
203        InputMapUtils.enableEnter(btnCancel);
204
205        // -- cancel on ESC
206        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
207        getRootPane().getActionMap().put("cancel", actCancel);
208
209        // -- help button
210        SideButton btnHelp;
211        pnl.add(btnHelp = new SideButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString())));
212        InputMapUtils.enableEnter(btnHelp);
213
214        return pnl;
215    }
216
217    /**
218     * Constructs a new {@code DownloadDialog}.
219     * @param parent the parent component
220     */
221    public DownloadDialog(Component parent) {
222        this(parent, ht("/Action/Download"));
223    }
224
225    /**
226     * Constructs a new {@code DownloadDialog}.
227     * @param parent the parent component
228     * @param helpTopic the help topic to assign
229     */
230    public DownloadDialog(Component parent, String helpTopic) {
231        super(JOptionPane.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL);
232        HelpUtil.setHelpContext(getRootPane(), helpTopic);
233        getContentPane().setLayout(new BorderLayout());
234        getContentPane().add(buildMainPanel(), BorderLayout.CENTER);
235        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
236
237        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
238                KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "checkClipboardContents");
239
240        getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
241            @Override
242            public void actionPerformed(ActionEvent e) {
243                String clip = Utils.getClipboardContent();
244                if (clip == null) {
245                    return;
246                }
247                Bounds b = OsmUrlToBounds.parse(clip);
248                if (b != null) {
249                    boundingBoxChanged(new Bounds(b), null);
250                }
251            }
252        });
253        addWindowListener(new WindowEventHandler());
254        restoreSettings();
255    }
256
257    private void updateSizeCheck() {
258        boolean isAreaTooLarge = false;
259        if (currentBounds == null) {
260            sizeCheck.setText(tr("No area selected yet"));
261            sizeCheck.setForeground(Color.darkGray);
262        } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
263            // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
264            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area-notes", 25);
265        } else {
266            // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
267            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area", 0.25);
268        }
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        Main.pref.put("download.tab", Integer.toString(tpDownloadAreaSelectors.getSelectedIndex()));
365        Main.pref.put("download.osm", cbDownloadOsmData.isSelected());
366        Main.pref.put("download.gps", cbDownloadGpxData.isSelected());
367        Main.pref.put("download.notes", cbDownloadNotes.isSelected());
368        Main.pref.put("download.newlayer", 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(Main.pref.getBoolean("download.osm", true));
379        cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false));
380        cbDownloadNotes.setSelected(Main.pref.getBoolean("download.notes", false));
381        cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false));
382        cbStartup.setSelected(isAutorunEnabled());
383        int idx = Main.pref.getInteger("download.tab", 0);
384        if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) {
385            idx = 0;
386        }
387        tpDownloadAreaSelectors.setSelectedIndex(idx);
388
389        if (Main.isDisplayingMapView()) {
390            MapView mv = Main.map.mapView;
391            currentBounds = new Bounds(
392                    mv.getLatLon(0, mv.getHeight()),
393                    mv.getLatLon(mv.getWidth(), 0)
394            );
395            boundingBoxChanged(currentBounds, null);
396        } else {
397            Bounds bounds = getSavedDownloadBounds();
398            if (bounds != null) {
399                currentBounds = bounds;
400                boundingBoxChanged(currentBounds, null);
401            }
402        }
403    }
404
405    /**
406     * Returns the previously saved bounding box from preferences.
407     * @return The bounding box saved in preferences if any, {@code null} otherwise
408     * @since 6509
409     */
410    public static Bounds getSavedDownloadBounds() {
411        String value = Main.pref.get("osm-download.bounds");
412        if (!value.isEmpty()) {
413            try {
414                return new Bounds(value, ";");
415            } catch (IllegalArgumentException e) {
416                Main.warn(e);
417            }
418        }
419        return null;
420    }
421
422    /**
423     * Determines if the dialog autorun is enabled in preferences.
424     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise
425     */
426    public static boolean isAutorunEnabled() {
427        return Main.pref.getBoolean("download.autorun", false);
428    }
429
430    /**
431     * Automatically opens the download dialog, if autorun is enabled.
432     * @see #isAutorunEnabled
433     */
434    public static void autostartIfNeeded() {
435        if (isAutorunEnabled()) {
436            Main.main.menu.download.actionPerformed(null);
437        }
438    }
439
440    /**
441     * Replies the currently selected download area.
442     * @return the currently selected download area. May be {@code null}, if no download area is selected yet.
443     */
444    public Bounds getSelectedDownloadArea() {
445        return currentBounds;
446    }
447
448    @Override
449    public void setVisible(boolean visible) {
450        if (visible) {
451            new WindowGeometry(
452                    getClass().getName() + ".geometry",
453                    WindowGeometry.centerInWindow(
454                            getParent(),
455                            new Dimension(1000, 600)
456                    )
457            ).applySafe(this);
458        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
459            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
460        }
461        super.setVisible(visible);
462    }
463
464    /**
465     * Replies true if the dialog was canceled
466     *
467     * @return true if the dialog was canceled
468     */
469    public boolean isCanceled() {
470        return canceled;
471    }
472
473    protected void setCanceled(boolean canceled) {
474        this.canceled = canceled;
475    }
476
477    protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
478    }
479
480    class CancelAction extends AbstractAction {
481        CancelAction() {
482            putValue(NAME, tr("Cancel"));
483            putValue(SMALL_ICON, ImageProvider.get("cancel"));
484            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
485        }
486
487        public void run() {
488            setCanceled(true);
489            setVisible(false);
490        }
491
492        @Override
493        public void actionPerformed(ActionEvent e) {
494            run();
495        }
496    }
497
498    class DownloadAction extends AbstractAction {
499        DownloadAction() {
500            putValue(NAME, tr("Download"));
501            putValue(SMALL_ICON, ImageProvider.get("download"));
502            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
503            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
504        }
505
506        public void run() {
507            if (currentBounds == null) {
508                JOptionPane.showMessageDialog(
509                        DownloadDialog.this,
510                        tr("Please select a download area first."),
511                        tr("Error"),
512                        JOptionPane.ERROR_MESSAGE
513                );
514                return;
515            }
516            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
517                JOptionPane.showMessageDialog(
518                        DownloadDialog.this,
519                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
520                                + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
521                                cbDownloadOsmData.getText(),
522                                cbDownloadGpxData.getText(),
523                                cbDownloadNotes.getText()
524                        ),
525                        tr("Error"),
526                        JOptionPane.ERROR_MESSAGE
527                );
528                return;
529            }
530            setCanceled(false);
531            setVisible(false);
532        }
533
534        @Override
535        public void actionPerformed(ActionEvent e) {
536            run();
537        }
538    }
539
540    class WindowEventHandler extends WindowAdapter {
541        @Override
542        public void windowClosing(WindowEvent e) {
543            new CancelAction().run();
544        }
545
546        @Override
547        public void windowActivated(WindowEvent e) {
548            btnDownload.requestFocusInWindow();
549        }
550    }
551}