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