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(new GridBagLayout()); 094 095 final ChangeListener checkboxChangeListener = new ChangeListener() { 096 @Override 097 public void stateChanged(ChangeEvent e) { 098 // size check depends on selected data source 099 updateSizeCheck(); 100 } 101 }; 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(Main.pref.getInteger("download.tab", 0)); 145 } catch (Exception ex) { 146 Main.pref.putInteger("download.tab", 0); 147 } 148 149 Font labelFont = sizeCheck.getFont(); 150 sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize())); 151 152 cbNewLayer = new JCheckBox(tr("Download as new layer")); 153 cbNewLayer.setToolTipText(tr("<html>Select to download data into a new data layer.<br>" 154 +"Unselect to download into the currently active data layer.</html>")); 155 156 cbStartup = new JCheckBox(tr("Open this dialog on startup")); 157 cbStartup.setToolTipText( 158 tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" + 159 "You can open it manually from File menu or toolbar.</html>")); 160 cbStartup.addActionListener(new ActionListener() { 161 @Override 162 public void actionPerformed(ActionEvent e) { 163 Main.pref.put("download.autorun", cbStartup.isSelected()); 164 } 165 }); 166 167 pnl.add(cbNewLayer, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5)); 168 pnl.add(cbStartup, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5)); 169 170 pnl.add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2)); 171 172 if (!ExpertToggleAction.isExpert()) { 173 JLabel infoLabel = new JLabel( 174 tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom.")); 175 pnl.add(infoLabel, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 0, 0)); 176 } 177 return pnl; 178 } 179 180 /* This should not be necessary, but if not here, repaint is not always correct in SlippyMap! */ 181 @Override 182 public void paint(Graphics g) { 183 tpDownloadAreaSelectors.getSelectedComponent().paint(g); 184 super.paint(g); 185 } 186 187 protected final JPanel buildButtonPanel() { 188 JPanel pnl = new JPanel(new FlowLayout()); 189 190 // -- download button 191 pnl.add(btnDownload = new SideButton(actDownload = new DownloadAction())); 192 InputMapUtils.enableEnter(btnDownload); 193 194 makeCheckBoxRespondToEnter(cbDownloadGpxData); 195 makeCheckBoxRespondToEnter(cbDownloadOsmData); 196 makeCheckBoxRespondToEnter(cbDownloadNotes); 197 makeCheckBoxRespondToEnter(cbNewLayer); 198 199 // -- cancel button 200 SideButton btnCancel; 201 CancelAction actCancel = new CancelAction(); 202 pnl.add(btnCancel = new SideButton(actCancel)); 203 InputMapUtils.enableEnter(btnCancel); 204 205 // -- cancel on ESC 206 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); 207 getRootPane().getActionMap().put("cancel", actCancel); 208 209 // -- help button 210 SideButton btnHelp; 211 pnl.add(btnHelp = new SideButton(new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString()))); 212 InputMapUtils.enableEnter(btnHelp); 213 214 return pnl; 215 } 216 217 /** 218 * Constructs a new {@code DownloadDialog}. 219 * @param parent the parent component 220 */ 221 public DownloadDialog(Component parent) { 222 this(parent, ht("/Action/Download")); 223 } 224 225 /** 226 * Constructs a new {@code DownloadDialog}. 227 * @param parent the parent component 228 * @param helpTopic the help topic to assign 229 */ 230 public DownloadDialog(Component parent, String helpTopic) { 231 super(JOptionPane.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL); 232 HelpUtil.setHelpContext(getRootPane(), helpTopic); 233 getContentPane().setLayout(new BorderLayout()); 234 getContentPane().add(buildMainPanel(), BorderLayout.CENTER); 235 getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH); 236 237 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 238 KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "checkClipboardContents"); 239 240 getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() { 241 @Override 242 public void actionPerformed(ActionEvent e) { 243 String clip = Utils.getClipboardContent(); 244 if (clip == null) { 245 return; 246 } 247 Bounds b = OsmUrlToBounds.parse(clip); 248 if (b != null) { 249 boundingBoxChanged(new Bounds(b), null); 250 } 251 } 252 }); 253 addWindowListener(new WindowEventHandler()); 254 restoreSettings(); 255 } 256 257 private void updateSizeCheck() { 258 boolean isAreaTooLarge = false; 259 if (currentBounds == null) { 260 sizeCheck.setText(tr("No area selected yet")); 261 sizeCheck.setForeground(Color.darkGray); 262 } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) { 263 // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml 264 isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area-notes", 25); 265 } else { 266 // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml 267 isAreaTooLarge = currentBounds.getArea() > Main.pref.getDouble("osm-server.max-request-area", 0.25); 268 } 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 Main.pref.put("download.tab", Integer.toString(tpDownloadAreaSelectors.getSelectedIndex())); 365 Main.pref.put("download.osm", cbDownloadOsmData.isSelected()); 366 Main.pref.put("download.gps", cbDownloadGpxData.isSelected()); 367 Main.pref.put("download.notes", cbDownloadNotes.isSelected()); 368 Main.pref.put("download.newlayer", 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(Main.pref.getBoolean("download.osm", true)); 379 cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false)); 380 cbDownloadNotes.setSelected(Main.pref.getBoolean("download.notes", false)); 381 cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false)); 382 cbStartup.setSelected(isAutorunEnabled()); 383 int idx = Main.pref.getInteger("download.tab", 0); 384 if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) { 385 idx = 0; 386 } 387 tpDownloadAreaSelectors.setSelectedIndex(idx); 388 389 if (Main.isDisplayingMapView()) { 390 MapView mv = Main.map.mapView; 391 currentBounds = new Bounds( 392 mv.getLatLon(0, mv.getHeight()), 393 mv.getLatLon(mv.getWidth(), 0) 394 ); 395 boundingBoxChanged(currentBounds, null); 396 } else { 397 Bounds bounds = getSavedDownloadBounds(); 398 if (bounds != null) { 399 currentBounds = bounds; 400 boundingBoxChanged(currentBounds, null); 401 } 402 } 403 } 404 405 /** 406 * Returns the previously saved bounding box from preferences. 407 * @return The bounding box saved in preferences if any, {@code null} otherwise 408 * @since 6509 409 */ 410 public static Bounds getSavedDownloadBounds() { 411 String value = Main.pref.get("osm-download.bounds"); 412 if (!value.isEmpty()) { 413 try { 414 return new Bounds(value, ";"); 415 } catch (IllegalArgumentException e) { 416 Main.warn(e); 417 } 418 } 419 return null; 420 } 421 422 /** 423 * Determines if the dialog autorun is enabled in preferences. 424 * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise 425 */ 426 public static boolean isAutorunEnabled() { 427 return Main.pref.getBoolean("download.autorun", false); 428 } 429 430 /** 431 * Automatically opens the download dialog, if autorun is enabled. 432 * @see #isAutorunEnabled 433 */ 434 public static void autostartIfNeeded() { 435 if (isAutorunEnabled()) { 436 Main.main.menu.download.actionPerformed(null); 437 } 438 } 439 440 /** 441 * Replies the currently selected download area. 442 * @return the currently selected download area. May be {@code null}, if no download area is selected yet. 443 */ 444 public Bounds getSelectedDownloadArea() { 445 return currentBounds; 446 } 447 448 @Override 449 public void setVisible(boolean visible) { 450 if (visible) { 451 new WindowGeometry( 452 getClass().getName() + ".geometry", 453 WindowGeometry.centerInWindow( 454 getParent(), 455 new Dimension(1000, 600) 456 ) 457 ).applySafe(this); 458 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 459 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 460 } 461 super.setVisible(visible); 462 } 463 464 /** 465 * Replies true if the dialog was canceled 466 * 467 * @return true if the dialog was canceled 468 */ 469 public boolean isCanceled() { 470 return canceled; 471 } 472 473 protected void setCanceled(boolean canceled) { 474 this.canceled = canceled; 475 } 476 477 protected void buildMainPanelAboveDownloadSelections(JPanel pnl) { 478 } 479 480 class CancelAction extends AbstractAction { 481 CancelAction() { 482 putValue(NAME, tr("Cancel")); 483 putValue(SMALL_ICON, ImageProvider.get("cancel")); 484 putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); 485 } 486 487 public void run() { 488 setCanceled(true); 489 setVisible(false); 490 } 491 492 @Override 493 public void actionPerformed(ActionEvent e) { 494 run(); 495 } 496 } 497 498 class DownloadAction extends AbstractAction { 499 DownloadAction() { 500 putValue(NAME, tr("Download")); 501 putValue(SMALL_ICON, ImageProvider.get("download")); 502 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); 503 setEnabled(!Main.isOffline(OnlineResource.OSM_API)); 504 } 505 506 public void run() { 507 if (currentBounds == null) { 508 JOptionPane.showMessageDialog( 509 DownloadDialog.this, 510 tr("Please select a download area first."), 511 tr("Error"), 512 JOptionPane.ERROR_MESSAGE 513 ); 514 return; 515 } 516 if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) { 517 JOptionPane.showMessageDialog( 518 DownloadDialog.this, 519 tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>" 520 + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>", 521 cbDownloadOsmData.getText(), 522 cbDownloadGpxData.getText(), 523 cbDownloadNotes.getText() 524 ), 525 tr("Error"), 526 JOptionPane.ERROR_MESSAGE 527 ); 528 return; 529 } 530 setCanceled(false); 531 setVisible(false); 532 } 533 534 @Override 535 public void actionPerformed(ActionEvent e) { 536 run(); 537 } 538 } 539 540 class WindowEventHandler extends WindowAdapter { 541 @Override 542 public void windowClosing(WindowEvent e) { 543 new CancelAction().run(); 544 } 545 546 @Override 547 public void windowActivated(WindowEvent e) { 548 btnDownload.requestFocusInWindow(); 549 } 550 } 551}