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();
094        pnl.setLayout(new GridBagLayout());
095
096        final ChangeListener checkboxChangeListener = new ChangeListener() {
097            @Override
098            public void stateChanged(ChangeEvent e) {
099                // size check depends on selected data source
100                updateSizeCheck();
101            }
102        };
103
104        // adding the download tasks
105        pnl.add(new JLabel(tr("Data Sources and Types:")), GBC.std().insets(5, 5, 1, 5));
106        cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
107        cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
108        cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
109        pnl.add(cbDownloadOsmData, GBC.std().insets(1, 5, 1, 5));
110        cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
111        cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
112        cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
113        pnl.add(cbDownloadGpxData, GBC.std().insets(5, 5, 1, 5));
114        cbDownloadNotes = new JCheckBox(tr("Notes"));
115        cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
116        cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
117        pnl.add(cbDownloadNotes, GBC.eol().insets(50, 5, 1, 5));
118
119        // must be created before hook
120        slippyMapChooser = new SlippyMapChooser();
121
122        // hook for subclasses
123        buildMainPanelAboveDownloadSelections(pnl);
124
125        // predefined download selections
126        downloadSelections.add(slippyMapChooser);
127        downloadSelections.add(new BookmarkSelection());
128        downloadSelections.add(new BoundingBoxSelection());
129        downloadSelections.add(new PlaceSelection());
130        downloadSelections.add(new TileSelection());
131
132        // add selections from plugins
133        PluginHandler.addDownloadSelection(downloadSelections);
134
135        // now everybody may add their tab to the tabbed pane
136        // (not done right away to allow plugins to remove one of
137        // the default selectors!)
138        for (DownloadSelection s : downloadSelections) {
139            s.addGui(this);
140        }
141
142        pnl.add(tpDownloadAreaSelectors, GBC.eol().fill());
143
144        try {
145            tpDownloadAreaSelectors.setSelectedIndex(Main.pref.getInteger("download.tab", 0));
146        } catch (Exception ex) {
147            Main.pref.putInteger("download.tab", 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(new ActionListener() {
162            @Override
163            public void actionPerformed(ActionEvent e) {
164                 Main.pref.put("download.autorun", cbStartup.isSelected());
165            }
166        });
167
168        pnl.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5));
169        pnl.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
170
171        pnl.add(sizeCheck,  GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
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            pnl.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0));
177        }
178        return pnl;
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    protected final JPanel buildButtonPanel() {
189        JPanel pnl = new JPanel();
190        pnl.setLayout(new FlowLayout());
191
192        // -- download button
193        pnl.add(btnDownload = new SideButton(actDownload = new DownloadAction()));
194        InputMapUtils.enableEnter(btnDownload);
195
196        makeCheckBoxRespondToEnter(cbDownloadGpxData);
197        makeCheckBoxRespondToEnter(cbDownloadOsmData);
198        makeCheckBoxRespondToEnter(cbDownloadNotes);
199        makeCheckBoxRespondToEnter(cbNewLayer);
200
201        // -- cancel button
202        SideButton btnCancel;
203        CancelAction actCancel = new CancelAction();
204        pnl.add(btnCancel = new SideButton(actCancel));
205        InputMapUtils.enableEnter(btnCancel);
206
207        // -- cancel on ESC
208        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
209        getRootPane().getActionMap().put("cancel", actCancel);
210
211        // -- help button
212        SideButton btnHelp;
213        pnl.add(btnHelp = new SideButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString())));
214        InputMapUtils.enableEnter(btnHelp);
215
216        return pnl;
217    }
218
219    /**
220     * Constructs a new {@code DownloadDialog}.
221     * @param parent the parent component
222     */
223    public DownloadDialog(Component parent) {
224        this(parent, ht("/Action/Download"));
225    }
226
227    /**
228     * Constructs a new {@code DownloadDialog}.
229     * @param parent the parent component
230     * @param helpTopic the help topic to assign
231     */
232    public DownloadDialog(Component parent, String helpTopic) {
233        super(JOptionPane.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL);
234        HelpUtil.setHelpContext(getRootPane(), helpTopic);
235        getContentPane().setLayout(new BorderLayout());
236        getContentPane().add(buildMainPanel(), BorderLayout.CENTER);
237        getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH);
238
239        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
240                KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "checkClipboardContents");
241
242        getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
243            @Override
244            public void actionPerformed(ActionEvent e) {
245                String clip = Utils.getClipboardContent();
246                if (clip == null) {
247                    return;
248                }
249                Bounds b = OsmUrlToBounds.parse(clip);
250                if (b != null) {
251                    boundingBoxChanged(new Bounds(b), null);
252                }
253            }
254        });
255        addWindowListener(new WindowEventHandler());
256        restoreSettings();
257    }
258
259    private void updateSizeCheck() {
260        boolean isAreaTooLarge = false;
261        if (currentBounds == null) {
262            sizeCheck.setText(tr("No area selected yet"));
263            sizeCheck.setForeground(Color.darkGray);
264        } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
265            // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
266            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area-notes", 25);
267        } else {
268            // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
269            isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area", 0.25);
270        }
271        if (isAreaTooLarge) {
272            sizeCheck.setText(tr("Download area too large; will probably be rejected by server"));
273            sizeCheck.setForeground(Color.red);
274        } else {
275            sizeCheck.setText(tr("Download area ok, size probably acceptable to server"));
276            sizeCheck.setForeground(Color.darkGray);
277        }
278    }
279
280    /**
281     * Distributes a "bounding box changed" from one DownloadSelection
282     * object to the others, so they may update or clear their input fields.
283     *
284     * @param eventSource - the DownloadSelection object that fired this notification.
285     */
286    public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) {
287        this.currentBounds = b;
288        for (DownloadSelection s : downloadSelections) {
289            if (s != eventSource) {
290                s.setDownloadArea(currentBounds);
291            }
292        }
293        updateSizeCheck();
294    }
295
296    /**
297     * Starts download for the given bounding box
298     * @param b bounding box to download
299     */
300    public void startDownload(Bounds b) {
301        this.currentBounds = b;
302        actDownload.run();
303    }
304
305    /**
306     * Replies true if the user selected to download OSM data
307     *
308     * @return true if the user selected to download OSM data
309     */
310    public boolean isDownloadOsmData() {
311        return cbDownloadOsmData.isSelected();
312    }
313
314    /**
315     * Replies true if the user selected to download GPX data
316     *
317     * @return true if the user selected to download GPX data
318     */
319    public boolean isDownloadGpxData() {
320        return cbDownloadGpxData.isSelected();
321    }
322
323    /**
324     * Replies true if user selected to download notes
325     *
326     * @return true if user selected to download notes
327     */
328    public boolean isDownloadNotes() {
329        return cbDownloadNotes.isSelected();
330    }
331
332    /**
333     * Replies true if the user requires to download into a new layer
334     *
335     * @return true if the user requires to download into a new layer
336     */
337    public boolean isNewLayerRequired() {
338        return cbNewLayer.isSelected();
339    }
340
341    /**
342     * Adds a new download area selector to the download dialog
343     *
344     * @param selector the download are selector
345     * @param displayName the display name of the selector
346     */
347    public void addDownloadAreaSelector(JPanel selector, String displayName) {
348        tpDownloadAreaSelectors.add(displayName, selector);
349    }
350
351    /**
352     * Refreshes the tile sources
353     * @since 6364
354     */
355    public final void refreshTileSources() {
356        if (slippyMapChooser != null) {
357            slippyMapChooser.refreshTileSources();
358        }
359    }
360
361    /**
362     * Remembers the current settings in the download dialog.
363     */
364    public void rememberSettings() {
365        Main.pref.put("download.tab", Integer.toString(tpDownloadAreaSelectors.getSelectedIndex()));
366        Main.pref.put("download.osm", cbDownloadOsmData.isSelected());
367        Main.pref.put("download.gps", cbDownloadGpxData.isSelected());
368        Main.pref.put("download.notes", cbDownloadNotes.isSelected());
369        Main.pref.put("download.newlayer", cbNewLayer.isSelected());
370        if (currentBounds != null) {
371            Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";"));
372        }
373    }
374
375    /**
376     * Restores the previous settings in the download dialog.
377     */
378    public void restoreSettings() {
379        cbDownloadOsmData.setSelected(Main.pref.getBoolean("download.osm", true));
380        cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false));
381        cbDownloadNotes.setSelected(Main.pref.getBoolean("download.notes", false));
382        cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false));
383        cbStartup.setSelected(isAutorunEnabled());
384        int idx = Main.pref.getInteger("download.tab", 0);
385        if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) {
386            idx = 0;
387        }
388        tpDownloadAreaSelectors.setSelectedIndex(idx);
389
390        if (Main.isDisplayingMapView()) {
391            MapView mv = Main.map.mapView;
392            currentBounds = new Bounds(
393                    mv.getLatLon(0, mv.getHeight()),
394                    mv.getLatLon(mv.getWidth(), 0)
395            );
396            boundingBoxChanged(currentBounds, null);
397        } else {
398            Bounds bounds = getSavedDownloadBounds();
399            if (bounds != null) {
400                currentBounds = bounds;
401                boundingBoxChanged(currentBounds, null);
402            }
403        }
404    }
405
406    /**
407     * Returns the previously saved bounding box from preferences.
408     * @return The bounding box saved in preferences if any, {@code null} otherwise
409     * @since 6509
410     */
411    public static Bounds getSavedDownloadBounds() {
412        String value = Main.pref.get("osm-download.bounds");
413        if (!value.isEmpty()) {
414            try {
415                return new Bounds(value, ";");
416            } catch (IllegalArgumentException e) {
417                Main.warn(e);
418            }
419        }
420        return null;
421    }
422
423    /**
424     * Determines if the dialog autorun is enabled in preferences.
425     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise
426     */
427    public static boolean isAutorunEnabled() {
428        return Main.pref.getBoolean("download.autorun", false);
429    }
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    }
476
477    class CancelAction extends AbstractAction {
478        CancelAction() {
479            putValue(NAME, tr("Cancel"));
480            putValue(SMALL_ICON, ImageProvider.get("cancel"));
481            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
482        }
483
484        public void run() {
485            setCanceled(true);
486            setVisible(false);
487        }
488
489        @Override
490        public void actionPerformed(ActionEvent e) {
491            run();
492        }
493    }
494
495    class DownloadAction extends AbstractAction {
496        DownloadAction() {
497            putValue(NAME, tr("Download"));
498            putValue(SMALL_ICON, ImageProvider.get("download"));
499            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
500            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
501        }
502
503        public void run() {
504            if (currentBounds == null) {
505                JOptionPane.showMessageDialog(
506                        DownloadDialog.this,
507                        tr("Please select a download area first."),
508                        tr("Error"),
509                        JOptionPane.ERROR_MESSAGE
510                );
511                return;
512            }
513            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
514                JOptionPane.showMessageDialog(
515                        DownloadDialog.this,
516                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
517                                + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
518                                cbDownloadOsmData.getText(),
519                                cbDownloadGpxData.getText(),
520                                cbDownloadNotes.getText()
521                        ),
522                        tr("Error"),
523                        JOptionPane.ERROR_MESSAGE
524                );
525                return;
526            }
527            setCanceled(false);
528            setVisible(false);
529        }
530
531        @Override
532        public void actionPerformed(ActionEvent e) {
533            run();
534        }
535    }
536
537    class WindowEventHandler extends WindowAdapter {
538        @Override
539        public void windowClosing(WindowEvent e) {
540            new CancelAction().run();
541        }
542
543        @Override
544        public void windowActivated(WindowEvent e) {
545            btnDownload.requestFocusInWindow();
546        }
547    }
548}