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 * @param b new current bounds 284 * 285 * @param eventSource - the DownloadSelection object that fired this notification. 286 */ 287 public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) { 288 this.currentBounds = b; 289 for (DownloadSelection s : downloadSelections) { 290 if (s != eventSource) { 291 s.setDownloadArea(currentBounds); 292 } 293 } 294 updateSizeCheck(); 295 } 296 297 /** 298 * Starts download for the given bounding box 299 * @param b bounding box to download 300 */ 301 public void startDownload(Bounds b) { 302 this.currentBounds = b; 303 actDownload.run(); 304 } 305 306 /** 307 * Replies true if the user selected to download OSM data 308 * 309 * @return true if the user selected to download OSM data 310 */ 311 public boolean isDownloadOsmData() { 312 return cbDownloadOsmData.isSelected(); 313 } 314 315 /** 316 * Replies true if the user selected to download GPX data 317 * 318 * @return true if the user selected to download GPX data 319 */ 320 public boolean isDownloadGpxData() { 321 return cbDownloadGpxData.isSelected(); 322 } 323 324 /** 325 * Replies true if user selected to download notes 326 * 327 * @return true if user selected to download notes 328 */ 329 public boolean isDownloadNotes() { 330 return cbDownloadNotes.isSelected(); 331 } 332 333 /** 334 * Replies true if the user requires to download into a new layer 335 * 336 * @return true if the user requires to download into a new layer 337 */ 338 public boolean isNewLayerRequired() { 339 return cbNewLayer.isSelected(); 340 } 341 342 /** 343 * Adds a new download area selector to the download dialog 344 * 345 * @param selector the download are selector 346 * @param displayName the display name of the selector 347 */ 348 public void addDownloadAreaSelector(JPanel selector, String displayName) { 349 tpDownloadAreaSelectors.add(displayName, selector); 350 } 351 352 /** 353 * Refreshes the tile sources 354 * @since 6364 355 */ 356 public final void refreshTileSources() { 357 if (slippyMapChooser != null) { 358 slippyMapChooser.refreshTileSources(); 359 } 360 } 361 362 /** 363 * Remembers the current settings in the download dialog. 364 */ 365 public void rememberSettings() { 366 Main.pref.put("download.tab", Integer.toString(tpDownloadAreaSelectors.getSelectedIndex())); 367 Main.pref.put("download.osm", cbDownloadOsmData.isSelected()); 368 Main.pref.put("download.gps", cbDownloadGpxData.isSelected()); 369 Main.pref.put("download.notes", cbDownloadNotes.isSelected()); 370 Main.pref.put("download.newlayer", cbNewLayer.isSelected()); 371 if (currentBounds != null) { 372 Main.pref.put("osm-download.bounds", currentBounds.encodeAsString(";")); 373 } 374 } 375 376 /** 377 * Restores the previous settings in the download dialog. 378 */ 379 public void restoreSettings() { 380 cbDownloadOsmData.setSelected(Main.pref.getBoolean("download.osm", true)); 381 cbDownloadGpxData.setSelected(Main.pref.getBoolean("download.gps", false)); 382 cbDownloadNotes.setSelected(Main.pref.getBoolean("download.notes", false)); 383 cbNewLayer.setSelected(Main.pref.getBoolean("download.newlayer", false)); 384 cbStartup.setSelected(isAutorunEnabled()); 385 int idx = Main.pref.getInteger("download.tab", 0); 386 if (idx < 0 || idx > tpDownloadAreaSelectors.getTabCount()) { 387 idx = 0; 388 } 389 tpDownloadAreaSelectors.setSelectedIndex(idx); 390 391 if (Main.isDisplayingMapView()) { 392 MapView mv = Main.map.mapView; 393 currentBounds = new Bounds( 394 mv.getLatLon(0, mv.getHeight()), 395 mv.getLatLon(mv.getWidth(), 0) 396 ); 397 boundingBoxChanged(currentBounds, null); 398 } else { 399 Bounds bounds = getSavedDownloadBounds(); 400 if (bounds != null) { 401 currentBounds = bounds; 402 boundingBoxChanged(currentBounds, null); 403 } 404 } 405 } 406 407 /** 408 * Returns the previously saved bounding box from preferences. 409 * @return The bounding box saved in preferences if any, {@code null} otherwise 410 * @since 6509 411 */ 412 public static Bounds getSavedDownloadBounds() { 413 String value = Main.pref.get("osm-download.bounds"); 414 if (!value.isEmpty()) { 415 try { 416 return new Bounds(value, ";"); 417 } catch (IllegalArgumentException e) { 418 Main.warn(e); 419 } 420 } 421 return null; 422 } 423 424 /** 425 * Determines if the dialog autorun is enabled in preferences. 426 * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise 427 */ 428 public static boolean isAutorunEnabled() { 429 return Main.pref.getBoolean("download.autorun", false); 430 } 431 432 /** 433 * Automatically opens the download dialog, if autorun is enabled. 434 * @see #isAutorunEnabled 435 */ 436 public static void autostartIfNeeded() { 437 if (isAutorunEnabled()) { 438 Main.main.menu.download.actionPerformed(null); 439 } 440 } 441 442 /** 443 * Replies the currently selected download area. 444 * @return the currently selected download area. May be {@code null}, if no download area is selected yet. 445 */ 446 public Bounds getSelectedDownloadArea() { 447 return currentBounds; 448 } 449 450 @Override 451 public void setVisible(boolean visible) { 452 if (visible) { 453 new WindowGeometry( 454 getClass().getName() + ".geometry", 455 WindowGeometry.centerInWindow( 456 getParent(), 457 new Dimension(1000, 600) 458 ) 459 ).applySafe(this); 460 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 461 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 462 } 463 super.setVisible(visible); 464 } 465 466 /** 467 * Replies true if the dialog was canceled 468 * 469 * @return true if the dialog was canceled 470 */ 471 public boolean isCanceled() { 472 return canceled; 473 } 474 475 protected void setCanceled(boolean canceled) { 476 this.canceled = canceled; 477 } 478 479 protected void buildMainPanelAboveDownloadSelections(JPanel pnl) { 480 } 481 482 class CancelAction extends AbstractAction { 483 CancelAction() { 484 putValue(NAME, tr("Cancel")); 485 putValue(SMALL_ICON, ImageProvider.get("cancel")); 486 putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); 487 } 488 489 public void run() { 490 setCanceled(true); 491 setVisible(false); 492 } 493 494 @Override 495 public void actionPerformed(ActionEvent e) { 496 run(); 497 } 498 } 499 500 class DownloadAction extends AbstractAction { 501 DownloadAction() { 502 putValue(NAME, tr("Download")); 503 putValue(SMALL_ICON, ImageProvider.get("download")); 504 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); 505 setEnabled(!Main.isOffline(OnlineResource.OSM_API)); 506 } 507 508 public void run() { 509 if (currentBounds == null) { 510 JOptionPane.showMessageDialog( 511 DownloadDialog.this, 512 tr("Please select a download area first."), 513 tr("Error"), 514 JOptionPane.ERROR_MESSAGE 515 ); 516 return; 517 } 518 if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) { 519 JOptionPane.showMessageDialog( 520 DownloadDialog.this, 521 tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>" 522 + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>", 523 cbDownloadOsmData.getText(), 524 cbDownloadGpxData.getText(), 525 cbDownloadNotes.getText() 526 ), 527 tr("Error"), 528 JOptionPane.ERROR_MESSAGE 529 ); 530 return; 531 } 532 setCanceled(false); 533 setVisible(false); 534 } 535 536 @Override 537 public void actionPerformed(ActionEvent e) { 538 run(); 539 } 540 } 541 542 class WindowEventHandler extends WindowAdapter { 543 @Override 544 public void windowClosing(WindowEvent e) { 545 new CancelAction().run(); 546 } 547 548 @Override 549 public void windowActivated(WindowEvent e) { 550 btnDownload.requestFocusInWindow(); 551 } 552 } 553}