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