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