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