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