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 *
051 */
052public class DownloadDialog extends JDialog  {
053    /** the unique instance of the download dialog */
054    static private 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    static public 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<DownloadSelection>();
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 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 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     */
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    public void restoreSettings() {
323        cbDownloadOsmData.setSelected(Main.pref.getBoolean("download.osm", true));
324        cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false));
325        cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false));
326        cbStartup.setSelected( isAutorunEnabled() );
327        int idx = Main.pref.getInteger("download.tab", 0);
328        if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) {
329            idx = 0;
330        }
331        tpDownloadAreaSelectors.setSelectedIndex(idx);
332
333        if (Main.isDisplayingMapView()) {
334            MapView mv = Main.map.mapView;
335            currentBounds = new Bounds(
336                    mv.getLatLon(0, mv.getHeight()),
337                    mv.getLatLon(mv.getWidth(), 0)
338            );
339            boundingBoxChanged(currentBounds,null);
340        }
341        else if (!Main.pref.get("osm-download.bounds").isEmpty()) {
342            // read the bounding box from the preferences
343            try {
344                currentBounds = new Bounds(Main.pref.get("osm-download.bounds"), ";");
345                boundingBoxChanged(currentBounds,null);
346            }
347            catch (Exception e) {
348                e.printStackTrace();
349            }
350        }
351    }
352
353    public static boolean isAutorunEnabled() {
354        return Main.pref.getBoolean("download.autorun",false);
355    }
356
357    public static void autostartIfNeeded() {
358        if (isAutorunEnabled()) {
359            Main.main.menu.download.actionPerformed(null);
360        }
361    }
362
363    /**
364     * Replies the currently selected download area. May be null, if no download area is selected yet.
365     */
366    public Bounds getSelectedDownloadArea() {
367        return currentBounds;
368    }
369
370    @Override
371    public void setVisible(boolean visible) {
372        if (visible) {
373            new WindowGeometry(
374                    getClass().getName() + ".geometry",
375                    WindowGeometry.centerInWindow(
376                            getParent(),
377                            new Dimension(1000,600)
378                    )
379            ).applySafe(this);
380        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
381            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
382        }
383        super.setVisible(visible);
384    }
385
386    /**
387     * Replies true if the dialog was canceled
388     *
389     * @return true if the dialog was canceled
390     */
391    public boolean isCanceled() {
392        return canceled;
393    }
394
395    protected void setCanceled(boolean canceled) {
396        this.canceled = canceled;
397    }
398
399    protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
400    }
401
402    class CancelAction extends AbstractAction {
403        public CancelAction() {
404            putValue(NAME, tr("Cancel"));
405            putValue(SMALL_ICON, ImageProvider.get("cancel"));
406            putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading"));
407        }
408
409        public void run() {
410            setCanceled(true);
411            setVisible(false);
412        }
413
414        @Override
415        public void actionPerformed(ActionEvent e) {
416            run();
417        }
418    }
419
420    class DownloadAction extends AbstractAction {
421        public DownloadAction() {
422            putValue(NAME, tr("Download"));
423            putValue(SMALL_ICON, ImageProvider.get("download"));
424            putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area"));
425        }
426
427        public void run() {
428            if (currentBounds == null) {
429                JOptionPane.showMessageDialog(
430                        DownloadDialog.this,
431                        tr("Please select a download area first."),
432                        tr("Error"),
433                        JOptionPane.ERROR_MESSAGE
434                );
435                return;
436            }
437            if (!isDownloadOsmData() && !isDownloadGpxData()) {
438                JOptionPane.showMessageDialog(
439                        DownloadDialog.this,
440                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> is enabled.<br>"
441                                + "Please choose to either download OSM data, or GPX data, or both.</html>",
442                                cbDownloadOsmData.getText(),
443                                cbDownloadGpxData.getText()
444                        ),
445                        tr("Error"),
446                        JOptionPane.ERROR_MESSAGE
447                );
448                return;
449            }
450            setCanceled(false);
451            setVisible(false);
452        }
453
454        @Override
455        public void actionPerformed(ActionEvent e) {
456            run();
457        }
458    }
459
460    class WindowEventHandler extends WindowAdapter {
461        @Override
462        public void windowClosing(WindowEvent e) {
463            new CancelAction().run();
464        }
465
466        @Override
467        public void windowActivated(WindowEvent e) {
468            btnDownload.requestFocusInWindow();
469        }
470    }
471}