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.GridBagLayout; 013import java.awt.event.ActionEvent; 014import java.awt.event.ComponentAdapter; 015import java.awt.event.ComponentEvent; 016import java.awt.event.WindowAdapter; 017import java.awt.event.WindowEvent; 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Optional; 021import java.util.stream.Collectors; 022import java.util.stream.IntStream; 023 024import javax.swing.AbstractAction; 025import javax.swing.BorderFactory; 026import javax.swing.Box; 027import javax.swing.Icon; 028import javax.swing.JButton; 029import javax.swing.JCheckBox; 030import javax.swing.JComponent; 031import javax.swing.JDialog; 032import javax.swing.JLabel; 033import javax.swing.JPanel; 034import javax.swing.JSplitPane; 035import javax.swing.JTabbedPane; 036import javax.swing.event.ChangeEvent; 037import javax.swing.event.ChangeListener; 038 039import org.openstreetmap.josm.actions.ExpertToggleAction; 040import org.openstreetmap.josm.data.Bounds; 041import org.openstreetmap.josm.data.coor.ILatLon; 042import org.openstreetmap.josm.data.coor.LatLon; 043import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager; 044import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat; 045import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat; 046import org.openstreetmap.josm.data.preferences.BooleanProperty; 047import org.openstreetmap.josm.data.preferences.IntegerProperty; 048import org.openstreetmap.josm.data.preferences.StringProperty; 049import org.openstreetmap.josm.gui.MainApplication; 050import org.openstreetmap.josm.gui.MapView; 051import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser; 052import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 053import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 054import org.openstreetmap.josm.gui.help.HelpUtil; 055import org.openstreetmap.josm.gui.layer.OsmDataLayer; 056import org.openstreetmap.josm.gui.util.GuiHelper; 057import org.openstreetmap.josm.gui.util.WindowGeometry; 058import org.openstreetmap.josm.gui.widgets.ImageLabel; 059import org.openstreetmap.josm.io.NetworkManager; 060import org.openstreetmap.josm.io.OnlineResource; 061import org.openstreetmap.josm.plugins.PluginHandler; 062import org.openstreetmap.josm.spi.preferences.Config; 063import org.openstreetmap.josm.tools.GBC; 064import org.openstreetmap.josm.tools.ImageProvider; 065import org.openstreetmap.josm.tools.InputMapUtils; 066import org.openstreetmap.josm.tools.JosmRuntimeException; 067import org.openstreetmap.josm.tools.ListenerList; 068import org.openstreetmap.josm.tools.Logging; 069import org.openstreetmap.josm.tools.OsmUrlToBounds; 070import org.openstreetmap.josm.tools.Shortcut; 071 072/** 073 * Dialog displayed to the user to download mapping data. 074 */ 075public class DownloadDialog extends JDialog { 076 077 private static final IntegerProperty DOWNLOAD_TAB = new IntegerProperty("download.tab", 0); 078 private static final StringProperty DOWNLOAD_SOURCE_TAB = new StringProperty("download.source.tab", OSMDownloadSource.SIMPLE_NAME); 079 private static final BooleanProperty DOWNLOAD_AUTORUN = new BooleanProperty("download.autorun", false); 080 private static final BooleanProperty DOWNLOAD_ZOOMTODATA = new BooleanProperty("download.zoomtodata", true); 081 082 /** the unique instance of the download dialog */ 083 private static DownloadDialog instance; 084 085 /** 086 * Replies the unique instance of the download dialog 087 * 088 * @return the unique instance of the download dialog 089 */ 090 public static synchronized DownloadDialog getInstance() { 091 if (instance == null) { 092 instance = new DownloadDialog(MainApplication.getMainFrame()); 093 } 094 return instance; 095 } 096 097 private static final ListenerList<DownloadSourceListener> downloadSourcesListeners = ListenerList.create(); 098 private static final List<DownloadSource<?>> downloadSources = new ArrayList<>(); 099 static { 100 // add default download sources 101 addDownloadSource(new OSMDownloadSource()); 102 addDownloadSource(new OverpassDownloadSource()); 103 } 104 105 protected final transient List<DownloadSelection> downloadSelections = new ArrayList<>(); 106 protected final JTabbedPane tpDownloadAreaSelectors = new JTabbedPane(); 107 protected final DownloadSourceTabs downloadSourcesTab = new DownloadSourceTabs(); 108 109 private final ImageLabel latText; 110 private final ImageLabel lonText; 111 private final ImageLabel bboxText; 112 { 113 final LatLon sample = new LatLon(90, 180); 114 final ICoordinateFormat sampleFormat = DMSCoordinateFormat.INSTANCE; 115 final Color background = new JPanel().getBackground(); 116 latText = new ImageLabel("lat", null, sampleFormat.latToString(sample).length(), background); 117 lonText = new ImageLabel("lon", null, sampleFormat.lonToString(sample).length(), background); 118 bboxText = new ImageLabel("bbox", null, sampleFormat.toString(sample, "").length() * 2, background); 119 } 120 121 protected JCheckBox cbStartup; 122 protected JCheckBox cbZoomToDownloadedData; 123 protected SlippyMapChooser slippyMapChooser; 124 protected JPanel mainPanel; 125 protected DownloadDialogSplitPane dialogSplit; 126 127 /* 128 * Keep the reference globally to avoid having it garbage collected 129 */ 130 protected final transient ExpertToggleAction.ExpertModeChangeListener expertListener = 131 getExpertModeListenerForDownloadSources(); 132 protected transient Bounds currentBounds; 133 protected boolean canceled; 134 135 protected JButton btnDownload; 136 protected JButton btnDownloadNewLayer; 137 protected JButton btnCancel; 138 protected JButton btnHelp; 139 140 /** 141 * Builds the main panel of the dialog. 142 * @return The panel of the dialog. 143 */ 144 protected final JPanel buildMainPanel() { 145 mainPanel = new JPanel(new GridBagLayout()); 146 147 // must be created before hook 148 slippyMapChooser = new SlippyMapChooser(); 149 150 // predefined download selections 151 downloadSelections.add(slippyMapChooser); 152 downloadSelections.add(new BookmarkSelection()); 153 downloadSelections.add(new BoundingBoxSelection()); 154 downloadSelections.add(new PlaceSelection()); 155 downloadSelections.add(new TileSelection()); 156 157 // add selections from plugins 158 PluginHandler.addDownloadSelection(downloadSelections); 159 160 // register all default download selections 161 for (DownloadSelection s : downloadSelections) { 162 s.addGui(this); 163 } 164 165 // allow to collapse the panes, but reserve some space for tabs 166 downloadSourcesTab.setMinimumSize(new Dimension(0, 25)); 167 tpDownloadAreaSelectors.setMinimumSize(new Dimension(0, 0)); 168 169 dialogSplit = new DownloadDialogSplitPane( 170 downloadSourcesTab, 171 tpDownloadAreaSelectors); 172 173 ChangeListener tabChangedListener = getDownloadSourceTabChangeListener(); 174 tabChangedListener.stateChanged(new ChangeEvent(downloadSourcesTab)); 175 downloadSourcesTab.addChangeListener(tabChangedListener); 176 177 mainPanel.add(dialogSplit, GBC.eol().fill()); 178 179 JPanel statusBarPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); 180 statusBarPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); 181 statusBarPanel.add(latText); 182 statusBarPanel.add(lonText); 183 statusBarPanel.add(bboxText); 184 mainPanel.add(statusBarPanel, GBC.eol().fill(GBC.HORIZONTAL)); 185 ExpertToggleAction.addVisibilitySwitcher(statusBarPanel); 186 187 cbStartup = new JCheckBox(tr("Open this dialog on startup")); 188 cbStartup.setToolTipText( 189 tr("<html>Autostart ''Download from OSM'' dialog every time JOSM is started.<br>" + 190 "You can open it manually from File menu or toolbar.</html>")); 191 cbStartup.addActionListener(e -> DOWNLOAD_AUTORUN.put(cbStartup.isSelected())); 192 193 JLabel iconZoomToDownloadedData = new JLabel(ImageProvider.get("dialogs/autoscale/download", ImageProvider.ImageSizes.SMALLICON)); 194 cbZoomToDownloadedData = new JCheckBox(tr("Zoom to downloaded data")); 195 cbZoomToDownloadedData.setToolTipText(tr("Select to zoom to entire newly downloaded data.")); 196 197 JPanel checkboxPanel = new JPanel(new FlowLayout()); 198 checkboxPanel.add(cbStartup); 199 checkboxPanel.add(Box.createHorizontalStrut(6)); 200 checkboxPanel.add(iconZoomToDownloadedData); 201 checkboxPanel.add(cbZoomToDownloadedData); 202 mainPanel.add(checkboxPanel, GBC.eol()); 203 204 ExpertToggleAction.addVisibilitySwitcher(cbZoomToDownloadedData); 205 ExpertToggleAction.addVisibilitySwitcher(iconZoomToDownloadedData); 206 207 JLabel infoLabel = new JLabel( 208 tr("Use left click&drag to select area, arrows or right mouse button to scroll map, wheel or +/- to zoom.")); 209 mainPanel.add(infoLabel, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 0)); 210 211 ExpertToggleAction.addExpertModeChangeListener(isExpert -> infoLabel.setVisible(!isExpert), true); 212 213 return mainPanel; 214 } 215 216 /** 217 * Builds the button pane of the dialog. 218 * @return The button panel of the dialog. 219 */ 220 protected final JPanel buildButtonPanel() { 221 btnDownload = new JButton(new DownloadAction(false)); 222 btnDownloadNewLayer = new JButton(new DownloadAction(true)); 223 btnCancel = new JButton(new CancelAction()); 224 btnHelp = new JButton( 225 new ContextSensitiveHelpAction(getRootPane().getClientProperty("help").toString())); 226 227 JPanel pnl = new JPanel(new FlowLayout()); 228 229 pnl.add(btnDownload); 230 pnl.add(btnDownloadNewLayer); 231 pnl.add(btnCancel); 232 pnl.add(btnHelp); 233 234 InputMapUtils.enableEnter(btnDownload); 235 InputMapUtils.enableEnter(btnCancel); 236 InputMapUtils.addEscapeAction(getRootPane(), btnCancel.getAction()); 237 InputMapUtils.enableEnter(btnHelp); 238 239 InputMapUtils.addEnterActionWhenAncestor(cbStartup, btnDownload.getAction()); 240 InputMapUtils.addEnterActionWhenAncestor(cbZoomToDownloadedData, btnDownload.getAction()); 241 InputMapUtils.addCtrlEnterAction(pnl, btnDownload.getAction()); 242 243 return pnl; 244 } 245 246 /** 247 * Constructs a new {@code DownloadDialog}. 248 * @param parent the parent component 249 */ 250 public DownloadDialog(Component parent) { 251 this(parent, ht("/Action/Download")); 252 } 253 254 /** 255 * Constructs a new {@code DownloadDialog}. 256 * @param parent the parent component 257 * @param helpTopic the help topic to assign 258 */ 259 public DownloadDialog(Component parent, String helpTopic) { 260 super(GuiHelper.getFrameForComponent(parent), tr("Download"), ModalityType.DOCUMENT_MODAL); 261 HelpUtil.setHelpContext(getRootPane(), helpTopic); 262 getContentPane().setLayout(new BorderLayout()); 263 getContentPane().add(buildMainPanel(), BorderLayout.CENTER); 264 getContentPane().add(buildButtonPanel(), BorderLayout.SOUTH); 265 266 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 267 Shortcut.getPasteKeyStroke(), "checkClipboardContents"); 268 269 getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() { 270 @Override 271 public void actionPerformed(ActionEvent e) { 272 String clip = ClipboardUtils.getClipboardStringContent(); 273 if (clip == null) { 274 return; 275 } 276 Bounds b = OsmUrlToBounds.parse(clip); 277 if (b != null) { 278 boundingBoxChanged(new Bounds(b), null); 279 } 280 } 281 }); 282 addWindowListener(new WindowEventHandler()); 283 ExpertToggleAction.addExpertModeChangeListener(expertListener); 284 restoreSettings(); 285 286 // if no bounding box is selected make sure it is still propagated. 287 if (currentBounds == null) { 288 boundingBoxChanged(null, null); 289 } 290 } 291 292 /** 293 * Distributes a "bounding box changed" from one DownloadSelection 294 * object to the others, so they may update or clear their input fields. Also informs 295 * download sources about the change, so they can react on it. 296 * @param b new current bounds 297 * 298 * @param eventSource - the DownloadSelection object that fired this notification. 299 */ 300 public void boundingBoxChanged(Bounds b, DownloadSelection eventSource) { 301 this.currentBounds = b; 302 for (DownloadSelection s : downloadSelections) { 303 if (s != eventSource) { 304 s.setDownloadArea(currentBounds); 305 } 306 } 307 308 for (AbstractDownloadSourcePanel<?> ds : downloadSourcesTab.getAllPanels()) { 309 ds.boundingBoxChanged(b); 310 } 311 312 bboxText.setText(b == null ? "" : String.join(" ", 313 CoordinateFormatManager.getDefaultFormat().toString(b.getMin(), " "), 314 CoordinateFormatManager.getDefaultFormat().toString(b.getMax(), " "))); 315 } 316 317 /** 318 * Updates the coordinates after moving the mouse cursor 319 * @param latLon the coordinates under the mouse cursor 320 */ 321 public void mapCursorChanged(ILatLon latLon) { 322 latText.setText(CoordinateFormatManager.getDefaultFormat().latToString(latLon)); 323 lonText.setText(CoordinateFormatManager.getDefaultFormat().lonToString(latLon)); 324 } 325 326 /** 327 * Starts download for the given bounding box 328 * @param b bounding box to download 329 */ 330 public void startDownload(Bounds b) { 331 this.currentBounds = b; 332 startDownload(); 333 } 334 335 /** 336 * Starts download. 337 */ 338 public void startDownload() { 339 btnDownload.doClick(); 340 } 341 342 /** 343 * Replies true if the user requires to zoom to new downloaded data 344 * 345 * @return true if the user requires to zoom to new downloaded data 346 * @since 11658 347 */ 348 public boolean isZoomToDownloadedDataRequired() { 349 return cbZoomToDownloadedData.isSelected(); 350 } 351 352 /** 353 * Determines if the dialog autorun is enabled in preferences. 354 * @return {@code true} if the download dialog must be open at startup, {@code false} otherwise. 355 */ 356 public static boolean isAutorunEnabled() { 357 return DOWNLOAD_AUTORUN.get(); 358 } 359 360 /** 361 * Adds a new download area selector to the download dialog. 362 * 363 * @param selector the download are selector. 364 * @param displayName the display name of the selector. 365 */ 366 public void addDownloadAreaSelector(JPanel selector, String displayName) { 367 tpDownloadAreaSelectors.add(displayName, selector); 368 } 369 370 /** 371 * Add a listener to get events from the DownloadSelection window 372 * 373 * @param selection The DownloadSelection object to send the Bounds to 374 * @return See {@link List#add} 375 * @since 16684 376 */ 377 public boolean addDownloadAreaListener(DownloadSelection selection) { 378 return downloadSelections.add(selection); 379 } 380 381 /** 382 * Remove a listener that was getting events from the DownloadSelection window 383 * 384 * @param selection The DownloadSelection object to not send the Bounds to 385 * @return See {@link List#remove} 386 * @since 16684 387 */ 388 public boolean removeDownloadAreaListener(DownloadSelection selection) { 389 return downloadSelections.remove(selection); 390 } 391 392 /** 393 * Adds a new download source to the download dialog if it is not added. 394 * 395 * @param downloadSource The download source to be added. 396 * @param <T> The type of the download data. 397 * @throws JosmRuntimeException If the download source is already added. Note, download sources are 398 * compared by their reference. 399 * @since 12878 400 */ 401 public static <T> void addDownloadSource(DownloadSource<T> downloadSource) { 402 if (downloadSources.contains(downloadSource)) { 403 throw new JosmRuntimeException("The download source you are trying to add already exists."); 404 } 405 406 downloadSources.add(downloadSource); 407 downloadSourcesListeners.fireEvent(l -> l.downloadSourceAdded(downloadSource)); 408 } 409 410 /** 411 * Remove a download source from the download dialog 412 * 413 * @param downloadSource The download source to be removed. 414 * @return see {@link List#remove} 415 * @since 15542 416 */ 417 public static boolean removeDownloadSource(DownloadSource<?> downloadSource) { 418 if (downloadSources.contains(downloadSource)) { 419 return downloadSources.remove(downloadSource); 420 } 421 return false; 422 } 423 424 /** 425 * Refreshes the tile sources. 426 * @since 6364 427 */ 428 public final void refreshTileSources() { 429 if (slippyMapChooser != null) { 430 slippyMapChooser.refreshTileSources(); 431 } 432 } 433 434 /** 435 * Remembers the current settings in the download dialog. 436 */ 437 public void rememberSettings() { 438 DOWNLOAD_TAB.put(tpDownloadAreaSelectors.getSelectedIndex()); 439 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::rememberSettings); 440 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> DOWNLOAD_SOURCE_TAB.put(panel.getSimpleName())); 441 DOWNLOAD_ZOOMTODATA.put(cbZoomToDownloadedData.isSelected()); 442 } 443 444 /** 445 * Restores the previous settings in the download dialog. 446 */ 447 public void restoreSettings() { 448 cbStartup.setSelected(isAutorunEnabled()); 449 cbZoomToDownloadedData.setSelected(DOWNLOAD_ZOOMTODATA.get()); 450 451 try { 452 tpDownloadAreaSelectors.setSelectedIndex(DOWNLOAD_TAB.get()); 453 } catch (IndexOutOfBoundsException e) { 454 Logging.trace(e); 455 tpDownloadAreaSelectors.setSelectedIndex(0); 456 } 457 458 downloadSourcesTab.getAllPanels().forEach(AbstractDownloadSourcePanel::restoreSettings); 459 downloadSourcesTab.setSelected(DOWNLOAD_SOURCE_TAB.get()); 460 461 if (MainApplication.isDisplayingMapView()) { 462 MapView mv = MainApplication.getMap().mapView; 463 currentBounds = new Bounds( 464 mv.getLatLon(0, mv.getHeight()), 465 mv.getLatLon(mv.getWidth(), 0) 466 ); 467 boundingBoxChanged(currentBounds, null); 468 } else { 469 Bounds bounds = getSavedDownloadBounds(); 470 if (bounds != null) { 471 currentBounds = bounds; 472 boundingBoxChanged(currentBounds, null); 473 } 474 } 475 } 476 477 /** 478 * Returns the previously saved bounding box from preferences. 479 * @return The bounding box saved in preferences if any, {@code null} otherwise. 480 * @since 6509 481 */ 482 public static Bounds getSavedDownloadBounds() { 483 String value = Config.getPref().get("osm-download.bounds"); 484 if (!value.isEmpty()) { 485 try { 486 return new Bounds(value, ";"); 487 } catch (IllegalArgumentException e) { 488 Logging.warn(e); 489 } 490 } 491 return null; 492 } 493 494 /** 495 * Automatically opens the download dialog, if autorun is enabled. 496 * @see #isAutorunEnabled 497 */ 498 public static void autostartIfNeeded() { 499 if (isAutorunEnabled()) { 500 MainApplication.getMenu().download.actionPerformed(null); 501 } 502 } 503 504 /** 505 * Returns an {@link Optional} of the currently selected download area. 506 * @return An {@link Optional} of the currently selected download area. 507 * @since 12574 Return type changed to optional 508 */ 509 public Optional<Bounds> getSelectedDownloadArea() { 510 return Optional.ofNullable(currentBounds); 511 } 512 513 @Override 514 public void setVisible(boolean visible) { 515 if (visible) { 516 btnDownloadNewLayer.setEnabled( 517 !MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).isEmpty()); 518 new WindowGeometry( 519 getClass().getName() + ".geometry", 520 WindowGeometry.centerInWindow( 521 getParent(), 522 new Dimension(1000, 600) 523 ) 524 ).applySafe(this); 525 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 526 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 527 } 528 super.setVisible(visible); 529 } 530 531 /** 532 * Replies true if the dialog was canceled 533 * 534 * @return true if the dialog was canceled 535 */ 536 public boolean isCanceled() { 537 return canceled; 538 } 539 540 /** 541 * Gets the global settings of the download dialog. 542 * @param newLayer The flag defining if a new layer must be created for the downloaded data. 543 * @return The {@link DownloadSettings} object that describes the current state of 544 * the download dialog. 545 */ 546 public DownloadSettings getDownloadSettings(boolean newLayer) { 547 final Component areaSelector = tpDownloadAreaSelectors.getSelectedComponent(); 548 final Bounds slippyMapBounds = areaSelector instanceof SlippyMapBBoxChooser 549 ? ((SlippyMapBBoxChooser) areaSelector).getVisibleMapArea() 550 : null; 551 return new DownloadSettings(currentBounds, slippyMapBounds, newLayer, isZoomToDownloadedDataRequired()); 552 } 553 554 protected void setCanceled(boolean canceled) { 555 this.canceled = canceled; 556 } 557 558 /** 559 * Adds the download source to the download sources tab. 560 * @param downloadSource The download source to be added. 561 * @param <T> The type of the download data. 562 */ 563 protected <T> void addNewDownloadSourceTab(DownloadSource<T> downloadSource) { 564 downloadSourcesTab.addPanel(downloadSource.createPanel(this)); 565 } 566 567 /** 568 * Creates listener that removes/adds download sources from/to {@code downloadSourcesTab} 569 * depending on the current mode. 570 * @return The expert mode listener. 571 */ 572 private ExpertToggleAction.ExpertModeChangeListener getExpertModeListenerForDownloadSources() { 573 return downloadSourcesTab::updateExpert; 574 } 575 576 /** 577 * Creates a listener that reacts on tab switches for {@code downloadSourcesTab} in order 578 * to adjust proper division of the dialog according to user saved preferences or minimal size 579 * of the panel. 580 * @return A listener to adjust dialog division. 581 */ 582 private ChangeListener getDownloadSourceTabChangeListener() { 583 return ec -> downloadSourcesTab.getSelectedPanel().ifPresent( 584 panel -> dialogSplit.setPolicy(panel.getSizingPolicy())); 585 } 586 587 /** 588 * Action that is executed when the cancel button is pressed. 589 */ 590 class CancelAction extends AbstractAction { 591 CancelAction() { 592 putValue(NAME, tr("Cancel")); 593 new ImageProvider("cancel").getResource().attachImageIcon(this); 594 putValue(SHORT_DESCRIPTION, tr("Click to close the dialog and to abort downloading")); 595 } 596 597 /** 598 * Cancels the download 599 */ 600 public void run() { 601 rememberSettings(); 602 setCanceled(true); 603 setVisible(false); 604 } 605 606 @Override 607 public void actionPerformed(ActionEvent e) { 608 Optional<AbstractDownloadSourcePanel<?>> panel = downloadSourcesTab.getSelectedPanel(); 609 run(); 610 panel.ifPresent(AbstractDownloadSourcePanel::checkCancel); 611 } 612 } 613 614 /** 615 * Action that is executed when the download button is pressed. 616 */ 617 class DownloadAction extends AbstractAction { 618 final boolean newLayer; 619 DownloadAction(boolean newLayer) { 620 this.newLayer = newLayer; 621 if (!newLayer) { 622 putValue(NAME, tr("Download")); 623 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area")); 624 new ImageProvider("download").getResource().attachImageIcon(this); 625 } else { 626 putValue(NAME, tr("Download as new layer")); 627 putValue(SHORT_DESCRIPTION, tr("Click to download the currently selected area into a new data layer")); 628 new ImageProvider("download_new_layer").getResource().attachImageIcon(this); 629 } 630 setEnabled(!NetworkManager.isOffline(OnlineResource.OSM_API)); 631 } 632 633 /** 634 * Starts the download and closes the dialog, if all requirements for the current download source are met. 635 * Otherwise the download is not started and the dialog remains visible. 636 */ 637 public void run() { 638 rememberSettings(); 639 downloadSourcesTab.getSelectedPanel().ifPresent(panel -> { 640 DownloadSettings downloadSettings = getDownloadSettings(newLayer); 641 if (panel.checkDownload(downloadSettings)) { 642 setCanceled(false); 643 setVisible(false); 644 panel.triggerDownload(downloadSettings); 645 } 646 }); 647 } 648 649 @Override 650 public void actionPerformed(ActionEvent e) { 651 run(); 652 } 653 } 654 655 class WindowEventHandler extends WindowAdapter { 656 @Override 657 public void windowClosing(WindowEvent e) { 658 new CancelAction().run(); 659 } 660 661 @Override 662 public void windowActivated(WindowEvent e) { 663 btnDownload.requestFocusInWindow(); 664 } 665 } 666 667 /** 668 * A special tabbed pane for {@link AbstractDownloadSourcePanel}s 669 * @author Michael Zangl 670 * @since 12706 671 */ 672 private class DownloadSourceTabs extends JTabbedPane implements DownloadSourceListener { 673 private final List<AbstractDownloadSourcePanel<?>> allPanels = new ArrayList<>(); 674 675 DownloadSourceTabs() { 676 downloadSources.forEach(this::downloadSourceAdded); 677 downloadSourcesListeners.addListener(this); 678 } 679 680 List<AbstractDownloadSourcePanel<?>> getAllPanels() { 681 return allPanels; 682 } 683 684 List<AbstractDownloadSourcePanel<?>> getVisiblePanels() { 685 return IntStream.range(0, getTabCount()) 686 .mapToObj(this::getComponentAt) 687 .map(p -> (AbstractDownloadSourcePanel<?>) p) 688 .collect(Collectors.toList()); 689 } 690 691 void setSelected(String simpleName) { 692 getVisiblePanels().stream() 693 .filter(panel -> simpleName.equals(panel.getSimpleName())) 694 .findFirst() 695 .ifPresent(this::setSelectedComponent); 696 } 697 698 void updateExpert(boolean isExpert) { 699 updateTabs(); 700 } 701 702 void addPanel(AbstractDownloadSourcePanel<?> panel) { 703 allPanels.add(panel); 704 updateTabs(); 705 } 706 707 private void updateTabs() { 708 // Not the best performance, but we don't do it often 709 removeAll(); 710 711 boolean isExpert = ExpertToggleAction.isExpert(); 712 allPanels.stream() 713 .filter(panel -> isExpert || !panel.getDownloadSource().onlyExpert()) 714 .forEach(panel -> addTab(panel.getDownloadSource().getLabel(), panel.getIcon(), panel)); 715 } 716 717 Optional<AbstractDownloadSourcePanel<?>> getSelectedPanel() { 718 return Optional.ofNullable((AbstractDownloadSourcePanel<?>) getSelectedComponent()); 719 } 720 721 @Override 722 public void insertTab(String title, Icon icon, Component component, String tip, int index) { 723 if (!(component instanceof AbstractDownloadSourcePanel)) { 724 throw new IllegalArgumentException("Can only add AbstractDownloadSourcePanels"); 725 } 726 super.insertTab(title, icon, component, tip, index); 727 } 728 729 @Override 730 public void downloadSourceAdded(DownloadSource<?> source) { 731 addPanel(source.createPanel(DownloadDialog.this)); 732 } 733 } 734 735 /** 736 * A special split pane that acts according to a {@link DownloadSourceSizingPolicy} 737 * 738 * It attempts to size the top tab content correctly. 739 * 740 * @author Michael Zangl 741 * @since 12705 742 */ 743 private static class DownloadDialogSplitPane extends JSplitPane { 744 private DownloadSourceSizingPolicy policy; 745 private final JTabbedPane topComponent; 746 /** 747 * If the height was explicitly set by the user. 748 */ 749 private boolean heightAdjustedExplicitly; 750 751 DownloadDialogSplitPane(JTabbedPane newTopComponent, Component newBottomComponent) { 752 super(VERTICAL_SPLIT, newTopComponent, newBottomComponent); 753 this.topComponent = newTopComponent; 754 755 addComponentListener(new ComponentAdapter() { 756 @Override 757 public void componentResized(ComponentEvent e) { 758 // doLayout is called automatically when the component size decreases 759 // This seems to be the only way to call doLayout when the component size increases 760 // We need this since we sometimes want to increase the top component size. 761 revalidate(); 762 } 763 }); 764 765 addPropertyChangeListener(DIVIDER_LOCATION_PROPERTY, e -> heightAdjustedExplicitly = true); 766 } 767 768 public void setPolicy(DownloadSourceSizingPolicy policy) { 769 this.policy = policy; 770 771 super.setDividerLocation(policy.getComponentHeight() + computeOffset()); 772 setDividerSize(policy.isHeightAdjustable() ? 10 : 0); 773 setEnabled(policy.isHeightAdjustable()); 774 } 775 776 @Override 777 public void doLayout() { 778 // We need to force this height before the layout manager is run. 779 // We cannot do this in the setDividerLocation, since the offset cannot be computed there. 780 int offset = computeOffset(); 781 if (policy.isHeightAdjustable() && heightAdjustedExplicitly) { 782 policy.storeHeight(Math.max(getDividerLocation() - offset, 0)); 783 } 784 // At least 30 pixel for map, if we have enough space 785 int maxValidDividerLocation = getHeight() > 150 ? getHeight() - 40 : getHeight(); 786 787 super.setDividerLocation(Math.min(policy.getComponentHeight() + offset, maxValidDividerLocation)); 788 super.doLayout(); 789 // Order is important (set this after setDividerLocation/doLayout called the listener) 790 this.heightAdjustedExplicitly = false; 791 } 792 793 /** 794 * @return The difference between the content height and the divider location 795 */ 796 private int computeOffset() { 797 Component selectedComponent = topComponent.getSelectedComponent(); 798 return topComponent.getHeight() - (selectedComponent == null ? 0 : selectedComponent.getHeight()); 799 } 800 } 801}