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