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