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.io.UnsupportedEncodingException; 008import java.net.URL; 009import java.net.URLEncoder; 010import java.util.Collection; 011import java.util.concurrent.Future; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.data.Bounds; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.osm.DataSet; 019import org.openstreetmap.josm.data.osm.DataSource; 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.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 private static final String PATTERN_OSM_API_URL = "http://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*"; 040 private static final String PATTERN_OVERPASS_API_URL = "http://.*/interpreter\\?data=.*"; 041 private static final String PATTERN_OVERPASS_API_XAPI_URL = "http://.*/xapi(\\?.*\\[@meta\\]|_meta\\?).*"; 042 private 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 protected final String encodePartialUrl(String url, String safePart) { 138 if (url != null && safePart != null) { 139 int pos = url.indexOf(safePart); 140 if (pos > -1) { 141 pos += safePart.length(); 142 try { 143 return url.substring(0, pos) + URLEncoder.encode(url.substring(pos), "UTF-8").replaceAll("\\+", "%20"); 144 } catch (UnsupportedEncodingException e) { 145 e.printStackTrace(); 146 } 147 } 148 } 149 return url; 150 } 151 152 /** 153 * Loads a given URL from the OSM Server 154 * @param new_layer True if the data should be saved to a new layer 155 * @param url The URL as String 156 */ 157 @Override 158 public Future<?> loadUrl(boolean new_layer, String url, ProgressMonitor progressMonitor) { 159 if (url.matches(PATTERN_OVERPASS_API_URL)) { 160 url = encodePartialUrl(url, "/interpreter?data="); // encode only the part after the = sign 161 162 } else if (url.matches(PATTERN_OVERPASS_API_XAPI_URL)) { 163 url = encodePartialUrl(url, "/xapi?"); // encode only the part after the ? sign 164 } 165 downloadTask = new DownloadTask(new_layer, 166 new OsmServerLocationReader(url), 167 progressMonitor); 168 currentBounds = null; 169 // Extract .osm filename from URL to set the new layer name 170 extractOsmFilename("https?://.*/(.*\\.osm)", url); 171 return Main.worker.submit(downloadTask); 172 } 173 174 protected final void extractOsmFilename(String pattern, String url) { 175 Matcher matcher = Pattern.compile(pattern).matcher(url); 176 newLayerName = matcher.matches() ? matcher.group(1) : null; 177 } 178 179 @Override 180 public void cancel() { 181 if (downloadTask != null) { 182 downloadTask.cancel(); 183 } 184 } 185 186 protected class DownloadTask extends PleaseWaitRunnable { 187 protected OsmServerReader reader; 188 protected DataSet dataSet; 189 protected boolean newLayer; 190 191 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { 192 super(tr("Downloading data"), progressMonitor, false); 193 this.reader = reader; 194 this.newLayer = newLayer; 195 } 196 197 protected DataSet parseDataSet() throws OsmTransferException { 198 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 199 } 200 201 @Override public void realRun() throws IOException, SAXException, OsmTransferException { 202 try { 203 if (isCanceled()) 204 return; 205 dataSet = parseDataSet(); 206 } catch(Exception e) { 207 if (isCanceled()) { 208 Main.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString())); 209 return; 210 } 211 if (e instanceof OsmTransferCanceledException) { 212 setCanceled(true); 213 return; 214 } else if (e instanceof OsmTransferException) { 215 rememberException(e); 216 } else { 217 rememberException(new OsmTransferException(e)); 218 } 219 DownloadOsmTask.this.setFailed(true); 220 } 221 } 222 223 protected OsmDataLayer getEditLayer() { 224 if (!Main.isDisplayingMapView()) return null; 225 return Main.main.getEditLayer(); 226 } 227 228 protected int getNumDataLayers() { 229 int count = 0; 230 if (!Main.isDisplayingMapView()) return 0; 231 Collection<Layer> layers = Main.map.mapView.getAllLayers(); 232 for (Layer layer : layers) { 233 if (layer instanceof OsmDataLayer) { 234 count++; 235 } 236 } 237 return count; 238 } 239 240 protected OsmDataLayer getFirstDataLayer() { 241 if (!Main.isDisplayingMapView()) return null; 242 Collection<Layer> layers = Main.map.mapView.getAllLayersAsList(); 243 for (Layer layer : layers) { 244 if (layer instanceof OsmDataLayer) 245 return (OsmDataLayer) layer; 246 } 247 return null; 248 } 249 250 protected OsmDataLayer createNewLayer(String layerName) { 251 if (layerName == null || layerName.isEmpty()) { 252 layerName = OsmDataLayer.createNewName(); 253 } 254 return new OsmDataLayer(dataSet, layerName, null); 255 } 256 257 protected OsmDataLayer createNewLayer() { 258 return createNewLayer(null); 259 } 260 261 @Override protected void finish() { 262 if (isFailed() || isCanceled()) 263 return; 264 if (dataSet == null) 265 return; // user canceled download or error occurred 266 if (dataSet.allPrimitives().isEmpty()) { 267 rememberErrorMessage(tr("No data found in this area.")); 268 // need to synthesize a download bounds lest the visual indication of downloaded 269 // area doesn't work 270 dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds : new Bounds(new LatLon(0, 0)), "OpenStreetMap server")); 271 } 272 273 rememberDownloadedData(dataSet); 274 int numDataLayers = getNumDataLayers(); 275 if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) { 276 // the user explicitly wants a new layer, we don't have any layer at all 277 // or it is not clear which layer to merge to 278 // 279 targetLayer = createNewLayer(newLayerName); 280 final boolean isDisplayingMapView = Main.isDisplayingMapView(); 281 282 Main.main.addLayer(targetLayer); 283 284 // If the mapView is not there yet, we cannot calculate the bounds (see constructor of MapView). 285 // Otherwise jump to the current download. 286 if (isDisplayingMapView) { 287 computeBboxAndCenterScale(); 288 } 289 } else { 290 targetLayer = getEditLayer(); 291 if (targetLayer == null) { 292 targetLayer = getFirstDataLayer(); 293 } 294 targetLayer.mergeFrom(dataSet); 295 computeBboxAndCenterScale(); 296 targetLayer.onPostDownloadFromServer(); 297 } 298 } 299 300 protected void computeBboxAndCenterScale() { 301 BoundingXYVisitor v = new BoundingXYVisitor(); 302 if (currentBounds != null) { 303 v.visit(currentBounds); 304 } else { 305 v.computeBoundingBox(dataSet.getNodes()); 306 } 307 Main.map.mapView.recalculateCenterScale(v); 308 } 309 310 @Override protected void cancel() { 311 setCanceled(true); 312 if (reader != null) { 313 reader.cancel(); 314 } 315 } 316 } 317 318 @Override 319 public String getConfirmationMessage(URL url) { 320 if (url != null) { 321 String urlString = url.toExternalForm(); 322 if (urlString.matches(PATTERN_OSM_API_URL)) { 323 // TODO: proper i18n after stabilization 324 String message = "<ul><li>"+tr("OSM Server URL:") + " " + url.getHost() + "</li><li>" + 325 tr("Command")+": "+url.getPath()+"</li>"; 326 if (url.getQuery() != null) { 327 message += "<li>" + tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")) + "</li>"; 328 } 329 message += "</ul>"; 330 return message; 331 } 332 // TODO: other APIs 333 } 334 return null; 335 } 336}