001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.download; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.Color; 008import java.awt.Dimension; 009import java.awt.FlowLayout; 010import java.awt.Font; 011import java.awt.GridBagLayout; 012import java.lang.reflect.InvocationTargetException; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.List; 017import java.util.concurrent.ExecutionException; 018import java.util.concurrent.Future; 019 020import javax.swing.Box; 021import javax.swing.Icon; 022import javax.swing.JCheckBox; 023import javax.swing.JLabel; 024import javax.swing.JOptionPane; 025import javax.swing.JPanel; 026import javax.swing.event.ChangeListener; 027 028import org.openstreetmap.josm.actions.downloadtasks.AbstractDownloadTask; 029import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask; 030import org.openstreetmap.josm.actions.downloadtasks.DownloadNotesTask; 031import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 032import org.openstreetmap.josm.actions.downloadtasks.DownloadParams; 033import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 034import org.openstreetmap.josm.data.Bounds; 035import org.openstreetmap.josm.data.ProjectionBounds; 036import org.openstreetmap.josm.data.ViewportData; 037import org.openstreetmap.josm.data.gpx.GpxData; 038import org.openstreetmap.josm.data.osm.DataSet; 039import org.openstreetmap.josm.data.osm.NoteData; 040import org.openstreetmap.josm.data.preferences.BooleanProperty; 041import org.openstreetmap.josm.gui.MainApplication; 042import org.openstreetmap.josm.gui.MapFrame; 043import org.openstreetmap.josm.gui.util.GuiHelper; 044import org.openstreetmap.josm.spi.preferences.Config; 045import org.openstreetmap.josm.tools.GBC; 046import org.openstreetmap.josm.tools.ImageProvider; 047import org.openstreetmap.josm.tools.Logging; 048import org.openstreetmap.josm.tools.Pair; 049 050/** 051 * Class defines the way data is fetched from the OSM server. 052 * @since 12652 053 */ 054public class OSMDownloadSource implements DownloadSource<List<IDownloadSourceType>> { 055 /** 056 * The simple name for the {@link OSMDownloadSourcePanel} 057 * @since 12706 058 */ 059 public static final String SIMPLE_NAME = "osmdownloadpanel"; 060 061 /** The possible methods to get data */ 062 static final List<IDownloadSourceType> DOWNLOAD_SOURCES = new ArrayList<>(); 063 static { 064 // Order is important (determines button order, and what gets zoomed to) 065 DOWNLOAD_SOURCES.add(new OsmDataDownloadType()); 066 DOWNLOAD_SOURCES.add(new GpsDataDownloadType()); 067 DOWNLOAD_SOURCES.add(new NotesDataDownloadType()); 068 } 069 070 @Override 071 public AbstractDownloadSourcePanel<List<IDownloadSourceType>> createPanel(DownloadDialog dialog) { 072 return new OSMDownloadSourcePanel(this, dialog); 073 } 074 075 @Override 076 public void doDownload(List<IDownloadSourceType> data, DownloadSettings settings) { 077 Bounds bbox = settings.getDownloadBounds() 078 .orElseThrow(() -> new IllegalArgumentException("OSM downloads requires bounds")); 079 boolean zoom = settings.zoomToData(); 080 boolean newLayer = settings.asNewLayer(); 081 final List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>(); 082 IDownloadSourceType zoomTask = zoom ? data.stream().findFirst().orElse(null) : null; 083 data.stream().filter(IDownloadSourceType::isEnabled).forEach(type -> { 084 try { 085 AbstractDownloadTask<?> task = type.getDownloadClass().getDeclaredConstructor().newInstance(); 086 task.setZoomAfterDownload(type.equals(zoomTask)); 087 Future<?> future = task.download(new DownloadParams().withNewLayer(newLayer), bbox, null); 088 MainApplication.worker.submit(new PostDownloadHandler(task, future)); 089 if (zoom) { 090 tasks.add(new Pair<AbstractDownloadTask<?>, Future<?>>(task, future)); 091 } 092 } catch (InstantiationException | IllegalAccessException | IllegalArgumentException 093 | InvocationTargetException | NoSuchMethodException | SecurityException e) { 094 Logging.error(e); 095 } 096 }); 097 098 if (zoom && tasks.size() > 1) { 099 MainApplication.worker.submit(() -> { 100 ProjectionBounds bounds = null; 101 // Wait for completion of download jobs 102 for (Pair<AbstractDownloadTask<?>, Future<?>> p : tasks) { 103 try { 104 p.b.get(); 105 ProjectionBounds b = p.a.getDownloadProjectionBounds(); 106 if (bounds == null) { 107 bounds = b; 108 } else if (b != null) { 109 bounds.extend(b); 110 } 111 } catch (InterruptedException | ExecutionException ex) { 112 Logging.warn(ex); 113 } 114 } 115 MapFrame map = MainApplication.getMap(); 116 // Zoom to the larger download bounds 117 if (map != null && bounds != null) { 118 final ProjectionBounds pb = bounds; 119 GuiHelper.runInEDTAndWait(() -> map.mapView.zoomTo(new ViewportData(pb))); 120 } 121 }); 122 } 123 } 124 125 @Override 126 public String getLabel() { 127 return tr("Download from OSM"); 128 } 129 130 @Override 131 public boolean onlyExpert() { 132 return false; 133 } 134 135 /** 136 * Returns the possible downloads that JOSM can make in the default Download screen. 137 * @return The possible downloads that JOSM can make in the default Download screen 138 * @since 16503 139 */ 140 public static List<IDownloadSourceType> getDownloadTypes() { 141 return Collections.unmodifiableList(DOWNLOAD_SOURCES); 142 } 143 144 /** 145 * Get the instance of a data download type 146 * 147 * @param <T> The type to get 148 * @param typeClazz The class of the type 149 * @return The type instance 150 * @since 16503 151 */ 152 public static <T extends IDownloadSourceType> T getDownloadType(Class<T> typeClazz) { 153 return DOWNLOAD_SOURCES.stream().filter(typeClazz::isInstance).map(typeClazz::cast).findFirst().orElse(null); 154 } 155 156 /** 157 * Removes a download source type. 158 * @param type The IDownloadSourceType object to remove 159 * @return {@code true} if this download types contained the specified object 160 * @since 16503 161 */ 162 public static boolean removeDownloadType(IDownloadSourceType type) { 163 if (type instanceof OsmDataDownloadType || type instanceof GpsDataDownloadType || type instanceof NotesDataDownloadType) { 164 throw new IllegalArgumentException(type.getClass().getName()); 165 } 166 return DOWNLOAD_SOURCES.remove(type); 167 } 168 169 /** 170 * Add a download type to the default JOSM download window 171 * 172 * @param type The initialized type to download 173 * @return {@code true} (as specified by {@link Collection#add}), but it also returns false if the class already has an instance in the list 174 * @since 16503 175 */ 176 public static boolean addDownloadType(IDownloadSourceType type) { 177 if (type instanceof OsmDataDownloadType || type instanceof GpsDataDownloadType || type instanceof NotesDataDownloadType) { 178 throw new IllegalArgumentException(type.getClass().getName()); 179 } else if (getDownloadType(type.getClass()) != null) { 180 return false; 181 } 182 return DOWNLOAD_SOURCES.add(type); 183 } 184 185 /** 186 * The GUI representation of the OSM download source. 187 * @since 12652 188 */ 189 public static class OSMDownloadSourcePanel extends AbstractDownloadSourcePanel<List<IDownloadSourceType>> { 190 private final JLabel sizeCheck = new JLabel(); 191 192 /** This is used to keep track of the components for download sources, and to dynamically update/remove them */ 193 private final JPanel downloadSourcesPanel; 194 195 private final ChangeListener checkboxChangeListener; 196 197 /** 198 * Label used in front of data types available for download. Made public for reuse in other download dialogs. 199 * @since 16155 200 */ 201 public static final String DATA_SOURCES_AND_TYPES = marktr("Data Sources and Types:"); 202 203 /** 204 * Creates a new {@link OSMDownloadSourcePanel}. 205 * @param dialog the parent download dialog, as {@code DownloadDialog.getInstance()} might not be initialized yet 206 * @param ds The osm download source the panel is for. 207 * @since 12900 208 */ 209 public OSMDownloadSourcePanel(OSMDownloadSource ds, DownloadDialog dialog) { 210 super(ds); 211 setLayout(new GridBagLayout()); 212 213 // size check depends on selected data source 214 checkboxChangeListener = e -> 215 dialog.getSelectedDownloadArea().ifPresent(this::updateSizeCheck); 216 217 downloadSourcesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 218 add(downloadSourcesPanel, GBC.eol().fill(GBC.HORIZONTAL)); 219 updateSources(); 220 221 sizeCheck.setFont(sizeCheck.getFont().deriveFont(Font.PLAIN)); 222 JPanel sizeCheckPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 223 sizeCheckPanel.add(sizeCheck); 224 add(sizeCheckPanel, GBC.eol().fill(GBC.HORIZONTAL)); 225 226 setMinimumSize(new Dimension(450, 115)); 227 } 228 229 /** 230 * Update the source list for downloading data 231 */ 232 protected void updateSources() { 233 downloadSourcesPanel.removeAll(); 234 downloadSourcesPanel.add(new JLabel(tr(DATA_SOURCES_AND_TYPES))); 235 DOWNLOAD_SOURCES.forEach(obj -> { 236 final Icon icon = obj.getIcon(); 237 if (icon != null) { 238 downloadSourcesPanel.add(Box.createHorizontalStrut(6)); 239 downloadSourcesPanel.add(new JLabel(icon)); 240 } 241 downloadSourcesPanel.add(obj.getCheckBox(checkboxChangeListener)); 242 }); 243 } 244 245 @Override 246 public List<IDownloadSourceType> getData() { 247 return DOWNLOAD_SOURCES; 248 } 249 250 @Override 251 public void rememberSettings() { 252 DOWNLOAD_SOURCES.forEach(type -> type.getBooleanProperty().put(type.getCheckBox().isSelected())); 253 } 254 255 @Override 256 public void restoreSettings() { 257 updateSources(); 258 DOWNLOAD_SOURCES.forEach(type -> type.getCheckBox().setSelected(type.isEnabled())); 259 } 260 261 @Override 262 public void setVisible(boolean aFlag) { 263 super.setVisible(aFlag); 264 updateSources(); 265 } 266 267 @Override 268 public boolean checkDownload(DownloadSettings settings) { 269 /* 270 * It is mandatory to specify the area to download from OSM. 271 */ 272 if (!settings.getDownloadBounds().isPresent()) { 273 JOptionPane.showMessageDialog( 274 this.getParent(), 275 tr("Please select a download area first."), 276 tr("Error"), 277 JOptionPane.ERROR_MESSAGE 278 ); 279 280 return false; 281 } 282 283 final Boolean slippyMapShowsDownloadBounds = settings.getSlippyMapBounds() 284 .map(b -> b.intersects(settings.getDownloadBounds().get())) 285 .orElse(true); 286 if (!slippyMapShowsDownloadBounds) { 287 final int confirmation = JOptionPane.showConfirmDialog( 288 this.getParent(), 289 tr("The slippy map no longer shows the selected download bounds. Continue?"), 290 tr("Confirmation"), 291 JOptionPane.OK_CANCEL_OPTION, 292 JOptionPane.QUESTION_MESSAGE 293 ); 294 if (confirmation != JOptionPane.OK_OPTION) { 295 return false; 296 } 297 } 298 299 /* 300 * Checks if the user selected the type of data to download. At least one the following 301 * must be chosen : raw osm data, gpx data, notes. 302 * If none of those are selected, then the corresponding dialog is shown to inform the user. 303 */ 304 if (DOWNLOAD_SOURCES.stream().noneMatch(IDownloadSourceType::isEnabled)) { 305 JOptionPane.showMessageDialog( 306 this.getParent(), 307 tr("Please select at least one download source."), 308 tr("Error"), 309 JOptionPane.ERROR_MESSAGE 310 ); 311 312 return false; 313 } 314 315 this.rememberSettings(); 316 317 return true; 318 } 319 320 @Override 321 public Icon getIcon() { 322 return ImageProvider.get("download"); 323 } 324 325 @Override 326 public void boundingBoxChanged(Bounds bbox) { 327 updateSizeCheck(bbox); 328 } 329 330 @Override 331 public String getSimpleName() { 332 return SIMPLE_NAME; 333 } 334 335 private void updateSizeCheck(Bounds bbox) { 336 if (bbox == null) { 337 sizeCheck.setText(tr("No area selected yet")); 338 sizeCheck.setForeground(Color.darkGray); 339 return; 340 } 341 342 displaySizeCheckResult(DOWNLOAD_SOURCES.stream() 343 .anyMatch(type -> type.isDownloadAreaTooLarge(bbox))); 344 } 345 346 private void displaySizeCheckResult(boolean isAreaTooLarge) { 347 if (isAreaTooLarge) { 348 sizeCheck.setText(tr("Download area too large; will probably be rejected by server")); 349 sizeCheck.setForeground(Color.red); 350 } else { 351 sizeCheck.setText(tr("Download area ok, size probably acceptable to server")); 352 sizeCheck.setForeground(Color.darkGray); 353 } 354 } 355 } 356 357 /** 358 * Encapsulates data that is required to download from the OSM server. 359 */ 360 static class OSMDownloadData { 361 362 private final List<IDownloadSourceType> downloadPossibilities; 363 364 /** 365 * Constructs a new {@code OSMDownloadData}. 366 * @param downloadPossibilities A list of DataDownloadTypes (instantiated, with 367 * options set) 368 */ 369 OSMDownloadData(List<IDownloadSourceType> downloadPossibilities) { 370 this.downloadPossibilities = downloadPossibilities; 371 } 372 373 /** 374 * Returns the download possibilities. 375 * @return A list of DataDownloadTypes (instantiated, with options set) 376 */ 377 public List<IDownloadSourceType> getDownloadPossibilities() { 378 return downloadPossibilities; 379 } 380 } 381 382 private static class OsmDataDownloadType implements IDownloadSourceType { 383 static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.data", true); 384 JCheckBox cbDownloadOsmData; 385 386 @Override 387 public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) { 388 if (cbDownloadOsmData == null) { 389 cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true); 390 cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area.")); 391 cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener); 392 } 393 if (checkboxChangeListener != null) { 394 cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener); 395 } 396 return cbDownloadOsmData; 397 } 398 399 @Override 400 public Icon getIcon() { 401 return ImageProvider.get("layer/osmdata_small", ImageProvider.ImageSizes.SMALLICON); 402 } 403 404 @Override 405 public Class<? extends AbstractDownloadTask<DataSet>> getDownloadClass() { 406 return DownloadOsmTask.class; 407 } 408 409 @Override 410 public BooleanProperty getBooleanProperty() { 411 return IS_ENABLED; 412 } 413 414 @Override 415 public boolean isDownloadAreaTooLarge(Bounds bound) { 416 // see max_request_area in 417 // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml 418 return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25); 419 } 420 } 421 422 private static class GpsDataDownloadType implements IDownloadSourceType { 423 static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.gps", false); 424 private JCheckBox cbDownloadGpxData; 425 426 @Override 427 public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) { 428 if (cbDownloadGpxData == null) { 429 cbDownloadGpxData = new JCheckBox(tr("Raw GPS data")); 430 cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area.")); 431 } 432 if (checkboxChangeListener != null) { 433 cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener); 434 } 435 436 return cbDownloadGpxData; 437 } 438 439 @Override 440 public Icon getIcon() { 441 return ImageProvider.get("layer/gpx_small", ImageProvider.ImageSizes.SMALLICON); 442 } 443 444 @Override 445 public Class<? extends AbstractDownloadTask<GpxData>> getDownloadClass() { 446 return DownloadGpsTask.class; 447 } 448 449 @Override 450 public BooleanProperty getBooleanProperty() { 451 return IS_ENABLED; 452 } 453 454 @Override 455 public boolean isDownloadAreaTooLarge(Bounds bound) { 456 return false; 457 } 458 } 459 460 private static class NotesDataDownloadType implements IDownloadSourceType { 461 static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.notes", false); 462 private JCheckBox cbDownloadNotes; 463 464 @Override 465 public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) { 466 if (cbDownloadNotes == null) { 467 cbDownloadNotes = new JCheckBox(tr("Notes")); 468 cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area.")); 469 } 470 if (checkboxChangeListener != null) { 471 cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener); 472 } 473 474 return cbDownloadNotes; 475 } 476 477 @Override 478 public Icon getIcon() { 479 return ImageProvider.get("dialogs/notes/note_open", ImageProvider.ImageSizes.SMALLICON); 480 } 481 482 @Override 483 public Class<? extends AbstractDownloadTask<NoteData>> getDownloadClass() { 484 return DownloadNotesTask.class; 485 } 486 487 @Override 488 public BooleanProperty getBooleanProperty() { 489 return IS_ENABLED; 490 } 491 492 @Override 493 public boolean isDownloadAreaTooLarge(Bounds bound) { 494 // see max_note_request_area in 495 // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml 496 return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area-notes", 25); 497 } 498 } 499}