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 Main.main.addLayer(layer, computeBbox(bounds)); 276 return layer; 277 } 278 return null; 279 } 280 281 protected void loadData(String newLayerName, Bounds bounds) { 282 OsmDataLayer layer = addNewLayerIfRequired(newLayerName, bounds); 283 if (layer == null) { 284 layer = getEditLayer(); 285 if (layer == null) { 286 layer = getFirstDataLayer(); 287 } 288 layer.mergeFrom(dataSet); 289 if (zoomAfterDownload) { 290 computeBboxAndCenterScale(bounds); 291 } 292 layer.onPostDownloadFromServer(); 293 } 294 } 295 } 296 297 protected class DownloadTask extends AbstractInternalTask { 298 protected final OsmServerReader reader; 299 300 /** 301 * Constructs a new {@code DownloadTask}. 302 * @param newLayer if {@code true}, force download to a new layer 303 * @param reader OSM data reader 304 * @param progressMonitor progress monitor 305 */ 306 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { 307 this(newLayer, reader, progressMonitor, true); 308 } 309 310 /** 311 * Constructs a new {@code DownloadTask}. 312 * @param newLayer if {@code true}, force download to a new layer 313 * @param reader OSM data reader 314 * @param progressMonitor progress monitor 315 * @param zoomAfterDownload If true, the map view will zoom to download area after download 316 * @since 8942 317 */ 318 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) { 319 super(newLayer, tr("Downloading data"), progressMonitor, false, zoomAfterDownload); 320 this.reader = reader; 321 } 322 323 protected DataSet parseDataSet() throws OsmTransferException { 324 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 325 } 326 327 @Override 328 public void realRun() throws IOException, SAXException, OsmTransferException { 329 try { 330 if (isCanceled()) 331 return; 332 dataSet = parseDataSet(); 333 } catch (Exception e) { 334 if (isCanceled()) { 335 Main.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString())); 336 return; 337 } 338 if (e instanceof OsmTransferCanceledException) { 339 setCanceled(true); 340 return; 341 } else if (e instanceof OsmTransferException) { 342 rememberException(e); 343 } else { 344 rememberException(new OsmTransferException(e)); 345 } 346 DownloadOsmTask.this.setFailed(true); 347 } 348 } 349 350 @Override 351 protected void finish() { 352 if (isFailed() || isCanceled()) 353 return; 354 if (dataSet == null) 355 return; // user canceled download or error occurred 356 if (dataSet.allPrimitives().isEmpty()) { 357 if (warnAboutEmptyArea) { 358 rememberErrorMessage(tr("No data found in this area.")); 359 } 360 // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work 361 dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds : 362 new Bounds(LatLon.ZERO), "OpenStreetMap server")); 363 } 364 365 rememberDownloadedData(dataSet); 366 loadData(newLayerName, currentBounds); 367 } 368 369 @Override 370 protected void cancel() { 371 setCanceled(true); 372 if (reader != null) { 373 reader.cancel(); 374 } 375 } 376 } 377 378 @Override 379 public String getConfirmationMessage(URL url) { 380 if (url != null) { 381 String urlString = url.toExternalForm(); 382 if (urlString.matches(PATTERN_OSM_API_URL)) { 383 // TODO: proper i18n after stabilization 384 Collection<String> items = new ArrayList<>(); 385 items.add(tr("OSM Server URL:") + ' ' + url.getHost()); 386 items.add(tr("Command")+": "+url.getPath()); 387 if (url.getQuery() != null) { 388 items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", "))); 389 } 390 return Utils.joinAsHtmlUnorderedList(items); 391 } 392 // TODO: other APIs 393 } 394 return null; 395 } 396}