// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.download; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.List; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.KeyStroke; import javax.swing.event.ChangeListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.ExpertToggleAction; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.preferences.BooleanProperty; import org.openstreetmap.josm.data.preferences.IntegerProperty; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.io.OnlineResource; import org.openstreetmap.josm.plugins.PluginHandler; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.InputMapUtils; import org.openstreetmap.josm.tools.OsmUrlToBounds; import org.openstreetmap.josm.tools.Utils; import org.openstreetmap.josm.tools.WindowGeometry; /** * Dialog displayed to download OSM and/or GPS data from OSM server. */ public class DownloadDialog extends JDialog { private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0); private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false); private static final BooleanProperty DOWNLOAD_OSM = new BooleanProperty("download.osm", true); private static final BooleanProperty DOWNLOAD_GPS = new BooleanProperty("download.gps", false); private static final BooleanProperty DOWNLOAD_NOTES = new BooleanProperty("download.notes", false); private static final BooleanProperty DOWNLOAD_NEWLAYER = new BooleanProperty("download.newlayer", false); private static final BooleanProperty DOWNLOAD_ZOOMTODATA = new BooleanProperty("download.zoomtodata", true); /** the unique instance of the download dialog */ private static DownloadDialog instance; /** * Replies the unique instance of the download dialog * * @return the unique instance of the download dialog */ public static synchronized DownloadDialog getInstance() { if (instance == null) { instance = new DownloadDialog(Main.parent); } return instance; } protected SlippyMapChooser slippyMapChooser; protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>(); protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane(); protected JCheckBox cbNewLayer; protected JCheckBox cbStartup; protected JCheckBox cbZoomToDownloadedData; protected final JLabel sizeCheck = new JLabel(); protected transient Bounds currentBounds; protected boolean canceled; protected JCheckBox cbDownloadOsmData; protected JCheckBox cbDownloadGpxData; protected JCheckBox cbDownloadNotes; /** the download action and button */ private final DownloadAction actDownload = new DownloadAction(); protected final JButton btnDownload = new JButton(actDownload); protected final JPanel buildMainPanel() { JPanel pnl = new JPanel(new GridBagLayout()); // size check depends on selected data source final ChangeListener checkboxChangeListener = e -> updateSizeCheck(); // adding the download tasks pnl.add(new JLabel(tr("Data Sources and Types:")), GBC.std().insets(5, 5, 1, 5)); cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true); cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area.")); cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener); pnl.add(cbDownloadOsmData, GBC.std().insets(1, 5, 1, 5)); cbDownloadGpxData = new JCheckBox(tr("Raw GPS data")); cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area.")); cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener); pnl.add(cbDownloadGpxData, GBC.std().insets(5, 5, 1, 5)); cbDownloadNotes = new JCheckBox(tr("Notes")); cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area.")); cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener); pnl.add(cbDownloadNotes, GBC.eol().insets(50, 5, 1, 5)); // must be created before hook slippyMapChooser = new SlippyMapChooser(); // hook for subclasses buildMainPanelAboveDownloadSelections(pnl); // predefined download selections downloadSelections.add(slippyMapChooser); downloadSelections.add(new BookmarkSelection()); downloadSelections.add(new BoundingBoxSelection()); downloadSelections.add(new PlaceSelection()); downloadSelections.add(new TileSelection()); // add selections from plugins PluginHandler.addDownloadSelection(downloadSelections); // now everybody may add their tab to the tabbed pane // (not done right away to allow plugins to remove one of // the default selectors!) for (DownloadSelection s : downloadSelections) { s.addGui(this); } pnl.add(tpDownloadAreaSelectors, GBC.eol().fill()); try { tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get()); } catch (IndexOutOfBoundsException ex) { Main.trace(ex); DOWNLOAD_TAB.put(0); } Font labelFont = sizeCheck.getFont(); sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize())); cbNewLayer = new JCheckBox(tr("Download as new layer")); cbNewLayer.setToolTipText(tr("<html>Select to download data into a new data layer.<br>" +"Unselect to download into the currently active data layer.</html>")); cbStartup = new JCheckBox(tr("Open this dialog on startup")); 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>")); cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected())); cbZoomToDownloadedData = new JCheckBox(tr("Zoom to downloaded data")); cbZoomToDownloadedData.setToolTipText(tr("Select to zoom to entire newly downloaded data.")); pnl.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5)); pnl.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5)); pnl.add(cbZoomToDownloadedData, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5)); ExpertToggleAction.addVisibilitySwitcher(cbZoomToDownloadedData); pnl.add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2)); if (!ExpertToggleAction.isExpert()) { JLabel infoLabel = new JLabel( tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom.")); pnl.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0)); } return pnl; } /* This should not be necessary, but if not here, repaint is not always correct in SlippyMap! */ @Override public void paint(Graphics g) { tpDownloadAreaSelectors.getSelectedComponent().paint(g); super.paint(g); } protected final JPanel buildButtonPanel() { JPanel pnl = new JPanel(new FlowLayout()); // -- download button pnl.add(btnDownload); InputMapUtils.enableEnter(btnDownload); InputMapUtils.addEnterActionWhenAncestor(cbDownloadGpxData, actDownload); InputMapUtils.addEnterActionWhenAncestor(cbDownloadOsmData, actDownload); InputMapUtils.addEnterActionWhenAncestor(cbDownloadNotes, actDownload); InputMapUtils.addEnterActionWhenAncestor(cbNewLayer, actDownload); InputMapUtils.addEnterActionWhenAncestor(cbStartup, actDownload); InputMapUtils.addEnterActionWhenAncestor(cbZoomToDownloadedData, actDownload); // -- cancel button JButton btnCancel; CancelAction actCancel = new CancelAction(); btnCancel = new JButton(actCancel); pnl.add(btnCancel); InputMapUtils.enableEnter(btnCancel); // -- cancel on ESC InputMapUtils.addEscapeAction(getRootPane(), actCancel); // -- help button JButton btnHelp = new JButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString())); pnl.add(btnHelp); InputMapUtils.enableEnter(btnHelp); return pnl; } /** * Constructs a new {@code DownloadDialog}. * @param parent the parent component */ public DownloadDialog(Component parent) { this(parent, ht("/Action/Download")); } /** * Constructs a new {@code DownloadDialog}. * @param parent the parent component * @param helpTopic the help topic to assign */ public DownloadDialog(Component parent, String helpTopic) { super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL); HelpUtil.setHelpContext(getRootPane(), helpTopic); getContentPane().setLayout(new BorderLayout()); getContentPane().add(buildMainPanel(), BorderLayout.CENTER); getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "checkClipboardContents"); getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { String clip = ClipboardUtils.getClipboardStringContent(); if (clip == null) { return; } Bounds b = OsmUrlToBounds.parse(clip); if (b != null) { boundingBoxChanged(new Bounds(b), null); } } }); addWindowListener(new WindowEventHandler()); restoreSettings(); } protected void updateSizeCheck() { boolean isAreaTooLarge = false; if (currentBounds == null) { sizeCheck.setText(tr("No area selected yet")); sizeCheck.setForeground(Color.darkGray); } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) { // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area-notes", 25); } else { // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area", 0.25); } displaySizeCheckResult(isAreaTooLarge); } protected void displaySizeCheckResult(boolean isAreaTooLarge) { if (isAreaTooLarge) { sizeCheck.setText(tr("Download area too large; will probably be rejected by server")); sizeCheck.setForeground(Color.red); } else { sizeCheck.setText(tr("Download area ok, size probably acceptable to server")); sizeCheck.setForeground(Color.darkGray); } } /** * Distributes a "bounding box changed" from one DownloadSelection * object to the others, so they may update or clear their input fields. * @param b new current bounds * * @param eventSource - the DownloadSelection object that fired this notification. */ public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) { this.currentBounds = b; for (DownloadSelection s : downloadSelections) { if (s != eventSource) { s.setDownloadArea(currentBounds); } } updateSizeCheck(); } /** * Starts download for the given bounding box * @param b bounding box to download */ public void startDownload(Bounds b) { this.currentBounds = b; actDownload.run(); } /** * Replies true if the user selected to download OSM data * * @return true if the user selected to download OSM data */ public boolean isDownloadOsmData() { return cbDownloadOsmData.isSelected(); } /** * Replies true if the user selected to download GPX data * * @return true if the user selected to download GPX data */ public boolean isDownloadGpxData() { return cbDownloadGpxData.isSelected(); } /** * Replies true if user selected to download notes * * @return true if user selected to download notes */ public boolean isDownloadNotes() { return cbDownloadNotes.isSelected(); } /** * Replies true if the user requires to download into a new layer * * @return true if the user requires to download into a new layer */ public boolean isNewLayerRequired() { return cbNewLayer.isSelected(); } /** * Replies true if the user requires to zoom to new downloaded data * * @return true if the user requires to zoom to new downloaded data * @since 11658 */ public boolean isZoomToDownloadedDataRequired() { return cbZoomToDownloadedData.isSelected(); } /** * Adds a new download area selector to the download dialog * * @param selector the download are selector * @param displayName the display name of the selector */ public void addDownloadAreaSelector(JPanel selector, String displayName) { tpDownloadAreaSelectors.add(displayName, selector); } /** * Refreshes the tile sources * @since 6364 */ public final void refreshTileSources() { if (slippyMapChooser != null) { slippyMapChooser.refreshTileSources(); } } /** * Remembers the current settings in the download dialog. */ public void rememberSettings() { DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex()); DOWNLOAD_OSM.put(cbDownloadOsmData.isSelected()); DOWNLOAD_GPS.put(cbDownloadGpxData.isSelected()); DOWNLOAD_NOTES.put(cbDownloadNotes.isSelected()); DOWNLOAD_NEWLAYER.put(cbNewLayer.isSelected()); DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected()); if (currentBounds != null) { Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";")); } } /** * Restores the previous settings in the download dialog. */ public void restoreSettings() { cbDownloadOsmData.setSelected(DOWNLOAD_OSM.get()); cbDownloadGpxData.setSelected(DOWNLOAD_GPS.get()); cbDownloadNotes.setSelected(DOWNLOAD_NOTES.get()); cbNewLayer.setSelected(DOWNLOAD_NEWLAYER.get()); cbStartup.setSelected(isAutorunEnabled()); cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get()); int idx = Utils.clamp(DOWNLOAD_TAB.get(), 0, tpDownloadAreaSelectors.getTabCount() - 1); tpDownloadAreaSelectors.setSelectedIndex(idx); if (Main.isDisplayingMapView()) { MapView mv = Main.map.mapView; currentBounds = new Bounds( mv.getLatLon(0, mv.getHeight()), mv.getLatLon(mv.getWidth(), 0) ); boundingBoxChanged(currentBounds, null); } else { Bounds bounds = getSavedDownloadBounds(); if (bounds != null) { currentBounds = bounds; boundingBoxChanged(currentBounds, null); } } } /** * Returns the previously saved bounding box from preferences. * @return The bounding box saved in preferences if any, {@code null} otherwise * @since 6509 */ public static Bounds getSavedDownloadBounds() { String value = Main.pref.get("osm-download.bounds"); if (!value.isEmpty()) { try { return new Bounds(value, ";"); } catch (IllegalArgumentException e) { Main.warn(e); } } return null; } /** * Determines if the dialog autorun is enabled in preferences. * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise */ public static boolean isAutorunEnabled() { return DOWNLOAD_AUTORUN.get(); } /** * Automatically opens the download dialog, if autorun is enabled. * @see #isAutorunEnabled */ public static void autostartIfNeeded() { if (isAutorunEnabled()) { Main.main.menu.download.actionPerformed(null); } } /** * Replies the currently selected download area. * @return the currently selected download area. May be {@code null}, if no download area is selected yet. */ public Bounds getSelectedDownloadArea() { return currentBounds; } @Override public void setVisible(boolean visible) { if (visible) { new WindowGeometry( getClass().getName() + ".geometry", WindowGeometry.centerInWindow( getParent(), new Dimension(1000, 600) ) ).applySafe(this); } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); } super.setVisible(visible); } /** * Replies true if the dialog was canceled * * @return true if the dialog was canceled */ public boolean isCanceled() { return canceled; } protected void setCanceled(boolean canceled) { this.canceled = canceled; } protected void buildMainPanelAboveDownloadSelections(JPanel pnl) { // Do nothing } class CancelAction extends AbstractAction { CancelAction() { putValue(NAME, tr("Cancel")); new ImageProvider("cancel").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); } public void run() { setCanceled(true); setVisible(false); } @Override public void actionPerformed(ActionEvent e) { run(); } } class DownloadAction extends AbstractAction { DownloadAction() { putValue(NAME, tr("Download")); new ImageProvider("download").getResource().attachImageIcon(this); putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); setEnabled(!Main.isOffline(OnlineResource.OSM_API)); } public void run() { if (currentBounds == null) { JOptionPane.showMessageDialog( DownloadDialog.this, tr("Please select a download area first."), tr("Error"), JOptionPane.ERROR_MESSAGE ); return; } if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) { JOptionPane.showMessageDialog( DownloadDialog.this, tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>" + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>", cbDownloadOsmData.getText(), cbDownloadGpxData.getText(), cbDownloadNotes.getText() ), tr("Error"), JOptionPane.ERROR_MESSAGE ); return; } setCanceled(false); setVisible(false); } @Override public void actionPerformed(ActionEvent e) { run(); } } class WindowEventHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { new CancelAction().run(); } @Override public void windowActivated(WindowEvent e) { btnDownload.requestFocusInWindow(); } } }