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