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