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