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