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