001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.downloadtasks; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.net.URL; 008import java.util.ArrayList; 009import java.util.Collection; 010import java.util.concurrent.Future; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.Bounds; 016import org.openstreetmap.josm.data.DataSource; 017import org.openstreetmap.josm.data.ProjectionBounds; 018import org.openstreetmap.josm.data.coor.LatLon; 019import org.openstreetmap.josm.data.osm.DataSet; 020import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 021import org.openstreetmap.josm.gui.PleaseWaitRunnable; 022import org.openstreetmap.josm.gui.layer.Layer; 023import org.openstreetmap.josm.gui.layer.OsmDataLayer; 024import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 025import org.openstreetmap.josm.gui.progress.ProgressMonitor; 026import org.openstreetmap.josm.io.BoundingBoxDownloader; 027import org.openstreetmap.josm.io.OsmServerLocationReader; 028import org.openstreetmap.josm.io.OsmServerReader; 029import org.openstreetmap.josm.io.OsmTransferCanceledException; 030import org.openstreetmap.josm.io.OsmTransferException; 031import org.openstreetmap.josm.tools.Utils; 032import org.xml.sax.SAXException; 033 034/** 035 * Open the download dialog and download the data. 036 * Run in the worker thread. 037 */ 038public class DownloadOsmTask extends AbstractDownloadTask<DataSet> { 039 040 protected static final String PATTERN_OSM_API_URL = "https?://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*"; 041 protected static final String PATTERN_OVERPASS_API_URL = "https?://.*/interpreter\\?data=.*"; 042 protected static final String PATTERN_OVERPASS_API_XAPI_URL = "https?://.*/xapi(\\?.*\\[@meta\\]|_meta\\?).*"; 043 protected static final String PATTERN_EXTERNAL_OSM_FILE = "https?://.*/.*\\.osm"; 044 045 protected Bounds currentBounds; 046 protected DownloadTask downloadTask; 047 048 protected String newLayerName; 049 050 /** This allows subclasses to ignore this warning */ 051 protected boolean warnAboutEmptyArea = true; 052 053 @Override 054 public String[] getPatterns() { 055 if (this.getClass() == DownloadOsmTask.class) { 056 return new String[]{PATTERN_OSM_API_URL, PATTERN_OVERPASS_API_URL, 057 PATTERN_OVERPASS_API_XAPI_URL, PATTERN_EXTERNAL_OSM_FILE}; 058 } else { 059 return super.getPatterns(); 060 } 061 } 062 063 @Override 064 public String getTitle() { 065 if (this.getClass() == DownloadOsmTask.class) { 066 return tr("Download OSM"); 067 } else { 068 return super.getTitle(); 069 } 070 } 071 072 @Override 073 public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { 074 return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor); 075 } 076 077 /** 078 * Asynchronously launches the download task for a given bounding box. 079 * 080 * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor. 081 * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to 082 * be discarded. 083 * 084 * You can wait for the asynchronous download task to finish by synchronizing on the returned 085 * {@link Future}, but make sure not to freeze up JOSM. Example: 086 * <pre> 087 * Future<?> future = task.download(...); 088 * // DON'T run this on the Swing EDT or JOSM will freeze 089 * future.get(); // waits for the dowload task to complete 090 * </pre> 091 * 092 * The following example uses a pattern which is better suited if a task is launched from 093 * the Swing EDT: 094 * <pre> 095 * final Future<?> future = task.download(...); 096 * Runnable runAfterTask = new Runnable() { 097 * public void run() { 098 * // this is not strictly necessary because of the type of executor service 099 * // Main.worker is initialized with, but it doesn't harm either 100 * // 101 * future.get(); // wait for the download task to complete 102 * doSomethingAfterTheTaskCompleted(); 103 * } 104 * } 105 * Main.worker.submit(runAfterTask); 106 * </pre> 107 * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm}) 108 * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task 109 * selects one of the existing layers as download layer, preferably the active layer. 110 * @param downloadArea the area to download 111 * @param progressMonitor the progressMonitor 112 * @return the future representing the asynchronous task 113 */ 114 public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { 115 return download(new DownloadTask(newLayer, reader, progressMonitor), downloadArea); 116 } 117 118 protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) { 119 this.downloadTask = downloadTask; 120 this.currentBounds = new Bounds(downloadArea); 121 // We need submit instead of execute so we can wait for it to finish and get the error 122 // message if necessary. If no one calls getErrorMessage() it just behaves like execute. 123 return Main.worker.submit(downloadTask); 124 } 125 126 /** 127 * This allows subclasses to perform operations on the URL before {@link #loadUrl} is performed. 128 * @param url the original URL 129 * @return the modified URL 130 */ 131 protected String modifyUrlBeforeLoad(String url) { 132 return url; 133 } 134 135 /** 136 * Loads a given URL from the OSM Server 137 * @param newLayer True if the data should be saved to a new layer 138 * @param url The URL as String 139 */ 140 @Override 141 public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) { 142 String newUrl = modifyUrlBeforeLoad(url); 143 downloadTask = new DownloadTask(newLayer, 144 new OsmServerLocationReader(newUrl), 145 progressMonitor); 146 currentBounds = null; 147 // Extract .osm filename from URL to set the new layer name 148 extractOsmFilename("https?://.*/(.*\\.osm)", newUrl); 149 return Main.worker.submit(downloadTask); 150 } 151 152 protected final void extractOsmFilename(String pattern, String url) { 153 Matcher matcher = Pattern.compile(pattern).matcher(url); 154 newLayerName = matcher.matches() ? matcher.group(1) : null; 155 } 156 157 @Override 158 public void cancel() { 159 if (downloadTask != null) { 160 downloadTask.cancel(); 161 } 162 } 163 164 @Override 165 public boolean isSafeForRemotecontrolRequests() { 166 return true; 167 } 168 169 /** 170 * Superclass of internal download task. 171 * @since 7636 172 */ 173 public abstract static class AbstractInternalTask extends PleaseWaitRunnable { 174 175 protected final boolean newLayer; 176 protected final boolean zoomAfterDownload; 177 protected DataSet dataSet; 178 179 /** 180 * Constructs a new {@code AbstractInternalTask}. 181 * 182 * @param newLayer if {@code true}, force download to a new layer 183 * @param title message for the user 184 * @param ignoreException If true, exception will be propagated to calling code. If false then 185 * exception will be thrown directly in EDT. When this runnable is executed using executor framework 186 * then use false unless you read result of task (because exception will get lost if you don't) 187 * @param zoomAfterDownload If true, the map view will zoom to download area after download 188 */ 189 public AbstractInternalTask(boolean newLayer, String title, boolean ignoreException, boolean zoomAfterDownload) { 190 super(title, ignoreException); 191 this.newLayer = newLayer; 192 this.zoomAfterDownload = zoomAfterDownload; 193 } 194 195 /** 196 * Constructs a new {@code AbstractInternalTask}. 197 * 198 * @param newLayer if {@code true}, force download to a new layer 199 * @param title message for the user 200 * @param progressMonitor progress monitor 201 * @param ignoreException If true, exception will be propagated to calling code. If false then 202 * exception will be thrown directly in EDT. When this runnable is executed using executor framework 203 * then use false unless you read result of task (because exception will get lost if you don't) 204 * @param zoomAfterDownload If true, the map view will zoom to download area after download 205 */ 206 public AbstractInternalTask(boolean newLayer, String title, ProgressMonitor progressMonitor, boolean ignoreException, 207 boolean zoomAfterDownload) { 208 super(title, progressMonitor, ignoreException); 209 this.newLayer = newLayer; 210 this.zoomAfterDownload = zoomAfterDownload; 211 } 212 213 protected OsmDataLayer getEditLayer() { 214 if (!Main.isDisplayingMapView()) return null; 215 return Main.main.getEditLayer(); 216 } 217 218 protected int getNumDataLayers() { 219 if (!Main.isDisplayingMapView()) return 0; 220 int count = 0; 221 Collection<Layer> layers = Main.map.mapView.getAllLayers(); 222 for (Layer layer : layers) { 223 if (layer instanceof OsmDataLayer) { 224 count++; 225 } 226 } 227 return count; 228 } 229 230 protected OsmDataLayer getFirstDataLayer() { 231 if (!Main.isDisplayingMapView()) return null; 232 Collection<Layer> layers = Main.map.mapView.getAllLayersAsList(); 233 for (Layer layer : layers) { 234 if (layer instanceof OsmDataLayer) 235 return (OsmDataLayer) layer; 236 } 237 return null; 238 } 239 240 protected OsmDataLayer createNewLayer(String layerName) { 241 if (layerName == null || layerName.isEmpty()) { 242 layerName = OsmDataLayer.createNewName(); 243 } 244 return new OsmDataLayer(dataSet, layerName, null); 245 } 246 247 protected OsmDataLayer createNewLayer() { 248 return createNewLayer(null); 249 } 250 251 protected ProjectionBounds computeBbox(Bounds bounds) { 252 BoundingXYVisitor v = new BoundingXYVisitor(); 253 if (bounds != null) { 254 v.visit(bounds); 255 } else { 256 v.computeBoundingBox(dataSet.getNodes()); 257 } 258 return v.getBounds(); 259 } 260 261 protected void computeBboxAndCenterScale(Bounds bounds) { 262 ProjectionBounds pb = computeBbox(bounds); 263 BoundingXYVisitor v = new BoundingXYVisitor(); 264 v.visit(pb); 265 Main.map.mapView.zoomTo(v); 266 } 267 268 protected OsmDataLayer addNewLayerIfRequired(String newLayerName, Bounds bounds) { 269 int numDataLayers = getNumDataLayers(); 270 if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) { 271 // the user explicitly wants a new layer, we don't have any layer at all 272 // or it is not clear which layer to merge to 273 // 274 final OsmDataLayer layer = createNewLayer(newLayerName); 275 if (Main.main != null) 276 Main.main.addLayer(layer, computeBbox(bounds)); 277 return layer; 278 } 279 return null; 280 } 281 282 protected void loadData(String newLayerName, Bounds bounds) { 283 OsmDataLayer layer = addNewLayerIfRequired(newLayerName, bounds); 284 if (layer == null) { 285 layer = getEditLayer(); 286 if (layer == null) { 287 layer = getFirstDataLayer(); 288 } 289 layer.mergeFrom(dataSet); 290 if (zoomAfterDownload) { 291 computeBboxAndCenterScale(bounds); 292 } 293 layer.onPostDownloadFromServer(); 294 } 295 } 296 } 297 298 protected class DownloadTask extends AbstractInternalTask { 299 protected final OsmServerReader reader; 300 301 /** 302 * Constructs a new {@code DownloadTask}. 303 * @param newLayer if {@code true}, force download to a new layer 304 * @param reader OSM data reader 305 * @param progressMonitor progress monitor 306 */ 307 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { 308 this(newLayer, reader, progressMonitor, true); 309 } 310 311 /** 312 * Constructs a new {@code DownloadTask}. 313 * @param newLayer if {@code true}, force download to a new layer 314 * @param reader OSM data reader 315 * @param progressMonitor progress monitor 316 * @param zoomAfterDownload If true, the map view will zoom to download area after download 317 * @since 8942 318 */ 319 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) { 320 super(newLayer, tr("Downloading data"), progressMonitor, false, zoomAfterDownload); 321 this.reader = reader; 322 } 323 324 protected DataSet parseDataSet() throws OsmTransferException { 325 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 326 } 327 328 @Override 329 public void realRun() throws IOException, SAXException, OsmTransferException { 330 try { 331 if (isCanceled()) 332 return; 333 dataSet = parseDataSet(); 334 } catch (Exception e) { 335 if (isCanceled()) { 336 Main.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString())); 337 return; 338 } 339 if (e instanceof OsmTransferCanceledException) { 340 setCanceled(true); 341 return; 342 } else if (e instanceof OsmTransferException) { 343 rememberException(e); 344 } else { 345 rememberException(new OsmTransferException(e)); 346 } 347 DownloadOsmTask.this.setFailed(true); 348 } 349 } 350 351 @Override 352 protected void finish() { 353 if (isFailed() || isCanceled()) 354 return; 355 if (dataSet == null) 356 return; // user canceled download or error occurred 357 if (dataSet.allPrimitives().isEmpty()) { 358 if (warnAboutEmptyArea) { 359 rememberErrorMessage(tr("No data found in this area.")); 360 } 361 // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work 362 dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds : 363 new Bounds(LatLon.ZERO), "OpenStreetMap server")); 364 } 365 366 rememberDownloadedData(dataSet); 367 loadData(newLayerName, currentBounds); 368 } 369 370 @Override 371 protected void cancel() { 372 setCanceled(true); 373 if (reader != null) { 374 reader.cancel(); 375 } 376 } 377 } 378 379 @Override 380 public String getConfirmationMessage(URL url) { 381 if (url != null) { 382 String urlString = url.toExternalForm(); 383 if (urlString.matches(PATTERN_OSM_API_URL)) { 384 // TODO: proper i18n after stabilization 385 Collection<String> items = new ArrayList<>(); 386 items.add(tr("OSM Server URL:") + ' ' + url.getHost()); 387 items.add(tr("Command")+": "+url.getPath()); 388 if (url.getQuery() != null) { 389 items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", "))); 390 } 391 return Utils.joinAsHtmlUnorderedList(items); 392 } 393 // TODO: other APIs 394 } 395 return null; 396 } 397}