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