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.gui.util.GuiHelper;
044import org.openstreetmap.josm.io.OnlineResource;
045import org.openstreetmap.josm.plugins.PluginHandler;
046import org.openstreetmap.josm.tools.GBC;
047import org.openstreetmap.josm.tools.ImageProvider;
048import org.openstreetmap.josm.tools.InputMapUtils;
049import org.openstreetmap.josm.tools.OsmUrlToBounds;
050import org.openstreetmap.josm.tools.Utils;
051import org.openstreetmap.josm.tools.WindowGeometry;
052
053/**
054 * Dialog displayed to download OSM and/or GPS data from OSM server.
055 */
056public class DownloadDialog extends JDialog  {
057    /** the unique instance of the download dialog */
058    private static DownloadDialog instance;
059
060    /**
061     * Replies the unique instance of the download dialog
062     *
063     * @return the unique instance of the download dialog
064     */
065    public static synchronized DownloadDialog getInstance() {
066        if (instance == null) {
067            instance = new DownloadDialog(Main.parent);
068        }
069        return instance;
070    }
071
072    protected SlippyMapChooser slippyMapChooser;
073    protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>();
074    protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane();
075    protected JCheckBox cbNewLayer;
076    protected JCheckBox cbStartup;
077    protected final JLabel sizeCheck = new JLabel();
078    protected transient Bounds currentBounds;
079    protected boolean canceled;
080
081    protected JCheckBox cbDownloadOsmData;
082    protected JCheckBox cbDownloadGpxData;
083    protected JCheckBox cbDownloadNotes;
084    /** the download action and button */
085    private final DownloadAction actDownload = new DownloadAction();
086    protected final SideButton btnDownload = new SideButton(actDownload);
087
088    private void makeCheckBoxRespondToEnter(JCheckBox cb) {
089        cb.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "doDownload");
090        cb.getActionMap().put("doDownload", actDownload);
091    }
092
093    protected final JPanel buildMainPanel() {
094        JPanel pnl = new JPanel(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 (IndexOutOfBoundsException 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(new FlowLayout());
190
191        // -- download button
192        pnl.add(btnDownload);
193        InputMapUtils.enableEnter(btnDownload);
194
195        makeCheckBoxRespondToEnter(cbDownloadGpxData);
196        makeCheckBoxRespondToEnter(cbDownloadOsmData);
197        makeCheckBoxRespondToEnter(cbDownloadNotes);
198        makeCheckBoxRespondToEnter(cbNewLayer);
199
200        // -- cancel button
201        SideButton btnCancel;
202        CancelAction actCancel = new CancelAction();
203        btnCancel = new SideButton(actCancel);
204        pnl.add(btnCancel);
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 = new SideButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString()));
213        pnl.add(btnHelp);
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(GuiHelper.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     * @param b new current bounds
284     *
285     * @param eventSource - the DownloadSelection object that fired this notification.
286     */
287    public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) {
288        this.currentBounds = b;
289        for (DownloadSelection s : downloadSelections) {
290            if (s != eventSource) {
291                s.setDownloadArea(currentBounds);
292            }
293        }
294        updateSizeCheck();
295    }
296
297    /**
298     * Starts download for the given bounding box
299     * @param b bounding box to download
300     */
301    public void startDownload(Bounds b) {
302        this.currentBounds = b;
303        actDownload.run();
304    }
305
306    /**
307     * Replies true if the user selected to download OSM data
308     *
309     * @return true if the user selected to download OSM data
310     */
311    public boolean isDownloadOsmData() {
312        return cbDownloadOsmData.isSelected();
313    }
314
315    /**
316     * Replies true if the user selected to download GPX data
317     *
318     * @return true if the user selected to download GPX data
319     */
320    public boolean isDownloadGpxData() {
321        return cbDownloadGpxData.isSelected();
322    }
323
324    /**
325     * Replies true if user selected to download notes
326     *
327     * @return true if user selected to download notes
328     */
329    public boolean isDownloadNotes() {
330        return cbDownloadNotes.isSelected();
331    }
332
333    /**
334     * Replies true if the user requires to download into a new layer
335     *
336     * @return true if the user requires to download into a new layer
337     */
338    public boolean isNewLayerRequired() {
339        return cbNewLayer.isSelected();
340    }
341
342    /**
343     * Adds a new download area selector to the download dialog
344     *
345     * @param selector the download are selector
346     * @param displayName the display name of the selector
347     */
348    public void addDownloadAreaSelector(JPanel selector, String displayName) {
349        tpDownloadAreaSelectors.add(displayName, selector);
350    }
351
352    /**
353     * Refreshes the tile sources
354     * @since 6364
355     */
356    public final void refreshTileSources() {
357        if (slippyMapChooser != null) {
358            slippyMapChooser.refreshTileSources();
359        }
360    }
361
362    /**
363     * Remembers the current settings in the download dialog.
364     */
365    public void rememberSettings() {
366        Main.pref.put("download.tab", Integer.toString(tpDownloadAreaSelectors.getSelectedIndex()));
367        Main.pref.put("download.osm", cbDownloadOsmData.isSelected());
368        Main.pref.put("download.gps", cbDownloadGpxData.isSelected());
369        Main.pref.put("download.notes", cbDownloadNotes.isSelected());
370        Main.pref.put("download.newlayer", cbNewLayer.isSelected());
371        if (currentBounds != null) {
372            Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";"));
373        }
374    }
375
376    /**
377     * Restores the previous settings in the download dialog.
378     */
379    public void restoreSettings() {
380        cbDownloadOsmData.setSelected(Main.pref.getBoolean("download.osm", true));
381        cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false));
382        cbDownloadNotes.setSelected(Main.pref.getBoolean("download.notes", false));
383        cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false));
384        cbStartup.setSelected(isAutorunEnabled());
385        int idx = Main.pref.getInteger("download.tab", 0);
386        if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) {
387            idx = 0;
388        }
389        tpDownloadAreaSelectors.setSelectedIndex(idx);
390
391        if (Main.isDisplayingMapView()) {
392            MapView mv = Main.map.mapView;
393            currentBounds = new Bounds(
394                    mv.getLatLon(0, mv.getHeight()),
395                    mv.getLatLon(mv.getWidth(), 0)
396            );
397            boundingBoxChanged(currentBounds, null);
398        } else {
399            Bounds bounds = getSavedDownloadBounds();
400            if (bounds != null) {
401                currentBounds = bounds;
402                boundingBoxChanged(currentBounds, null);
403            }
404        }
405    }
406
407    /**
408     * Returns the previously saved bounding box from preferences.
409     * @return The bounding box saved in preferences if any, {@code null} otherwise
410     * @since 6509
411     */
412    public static Bounds getSavedDownloadBounds() {
413        String value = Main.pref.get("osm-download.bounds");
414        if (!value.isEmpty()) {
415            try {
416                return new Bounds(value, ";");
417            } catch (IllegalArgumentException e) {
418                Main.warn(e);
419            }
420        }
421        return null;
422    }
423
424    /**
425     * Determines if the dialog autorun is enabled in preferences.
426     * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise
427     */
428    public static boolean isAutorunEnabled() {
429        return Main.pref.getBoolean("download.autorun", false);
430    }
431
432    /**
433     * Automatically opens the download dialog, if autorun is enabled.
434     * @see #isAutorunEnabled
435     */
436    public static void autostartIfNeeded() {
437        if (isAutorunEnabled()) {
438            Main.main.menu.download.actionPerformed(null);
439        }
440    }
441
442    /**
443     * Replies the currently selected download area.
444     * @return the currently selected download area. May be {@code null}, if no download area is selected yet.
445     */
446    public Bounds getSelectedDownloadArea() {
447        return currentBounds;
448    }
449
450    @Override
451    public void setVisible(boolean visible) {
452        if (visible) {
453            new WindowGeometry(
454                    getClass().getName() + ".geometry",
455                    WindowGeometry.centerInWindow(
456                            getParent(),
457                            new Dimension(1000, 600)
458                    )
459            ).applySafe(this);
460        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
461            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
462        }
463        super.setVisible(visible);
464    }
465
466    /**
467     * Replies true if the dialog was canceled
468     *
469     * @return true if the dialog was canceled
470     */
471    public boolean isCanceled() {
472        return canceled;
473    }
474
475    protected void setCanceled(boolean canceled) {
476        this.canceled = canceled;
477    }
478
479    protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
480        // Do nothing
481    }
482
483    class CancelAction extends AbstractAction {
484        CancelAction() {
485            putValue(NAME, tr("Cancel"));
486            putValue(SMALL_ICON, ImageProvider.get("cancel"));
487            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
488        }
489
490        public void run() {
491            setCanceled(true);
492            setVisible(false);
493        }
494
495        @Override
496        public void actionPerformed(ActionEvent e) {
497            run();
498        }
499    }
500
501    class DownloadAction extends AbstractAction {
502        DownloadAction() {
503            putValue(NAME, tr("Download"));
504            putValue(SMALL_ICON, ImageProvider.get("download"));
505            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
506            setEnabled(!Main.isOffline(OnlineResource.OSM_API));
507        }
508
509        public void run() {
510            if (currentBounds == null) {
511                JOptionPane.showMessageDialog(
512                        DownloadDialog.this,
513                        tr("Please select a download area first."),
514                        tr("Error"),
515                        JOptionPane.ERROR_MESSAGE
516                );
517                return;
518            }
519            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
520                JOptionPane.showMessageDialog(
521                        DownloadDialog.this,
522                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
523                                + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
524                                cbDownloadOsmData.getText(),
525                                cbDownloadGpxData.getText(),
526                                cbDownloadNotes.getText()
527                        ),
528                        tr("Error"),
529                        JOptionPane.ERROR_MESSAGE
530                );
531                return;
532            }
533            setCanceled(false);
534            setVisible(false);
535        }
536
537        @Override
538        public void actionPerformed(ActionEvent e) {
539            run();
540        }
541    }
542
543    class WindowEventHandler extends WindowAdapter {
544        @Override
545        public void windowClosing(WindowEvent e) {
546            new CancelAction().run();
547        }
548
549        @Override
550        public void windowActivated(WindowEvent e) {
551            btnDownload.requestFocusInWindow();
552        }
553    }
554}