001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions.downloadtasks; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.io.IOException; 007 import java.util.ArrayList; 008 import java.util.Collection; 009 import java.util.HashSet; 010 import java.util.Iterator; 011 import java.util.List; 012 import java.util.Set; 013 import java.util.concurrent.Future; 014 import java.util.regex.Matcher; 015 import java.util.regex.Pattern; 016 017 import javax.swing.JOptionPane; 018 import org.openstreetmap.josm.Main; 019 import org.openstreetmap.josm.data.Bounds; 020 import org.openstreetmap.josm.data.coor.LatLon; 021 import org.openstreetmap.josm.data.imagery.ImageryInfo; 022 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 023 import org.openstreetmap.josm.data.imagery.Shape; 024 import org.openstreetmap.josm.data.osm.DataSet; 025 import org.openstreetmap.josm.data.osm.DataSource; 026 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 027 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 028 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 029 import org.openstreetmap.josm.gui.layer.Layer; 030 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 031 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 032 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 033 import org.openstreetmap.josm.io.BoundingBoxDownloader; 034 import org.openstreetmap.josm.io.OsmServerLocationReader; 035 import org.openstreetmap.josm.io.OsmServerReader; 036 import org.openstreetmap.josm.io.OsmTransferCanceledException; 037 import org.openstreetmap.josm.io.OsmTransferException; 038 import org.openstreetmap.josm.tools.Utils; 039 import org.xml.sax.SAXException; 040 041 /** 042 * Open the download dialog and download the data. 043 * Run in the worker thread. 044 */ 045 public class DownloadOsmTask extends AbstractDownloadTask { 046 protected Bounds currentBounds; 047 protected DataSet downloadedData; 048 protected DownloadTask downloadTask; 049 050 protected OsmDataLayer targetLayer; 051 052 protected String newLayerName = null; 053 054 protected void rememberDownloadedData(DataSet ds) { 055 this.downloadedData = ds; 056 } 057 058 /** 059 * Replies the {@link DataSet} containing the downloaded OSM data. 060 * @return The {@link DataSet} containing the downloaded OSM data. 061 */ 062 public DataSet getDownloadedData() { 063 return downloadedData; 064 } 065 066 @Override 067 public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { 068 return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor); 069 } 070 071 /** 072 * Asynchronously launches the download task for a given bounding box. 073 * 074 * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor. 075 * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to 076 * be discarded. 077 * 078 * You can wait for the asynchronous download task to finish by synchronizing on the returned 079 * {@link Future}, but make sure not to freeze up JOSM. Example: 080 * <pre> 081 * Future<?> future = task.download(...); 082 * // DON'T run this on the Swing EDT or JOSM will freeze 083 * future.get(); // waits for the dowload task to complete 084 * </pre> 085 * 086 * The following example uses a pattern which is better suited if a task is launched from 087 * the Swing EDT: 088 * <pre> 089 * final Future<?> future = task.download(...); 090 * Runnable runAfterTask = new Runnable() { 091 * public void run() { 092 * // this is not strictly necessary because of the type of executor service 093 * // Main.worker is initialized with, but it doesn't harm either 094 * // 095 * future.get(); // wait for the download task to complete 096 * doSomethingAfterTheTaskCompleted(); 097 * } 098 * } 099 * Main.worker.submit(runAfterTask); 100 * </pre> 101 * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm}) 102 * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task 103 * selects one of the existing layers as download layer, preferably the active layer. 104 * @param downloadArea the area to download 105 * @param progressMonitor the progressMonitor 106 * @return the future representing the asynchronous task 107 */ 108 public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { 109 return download(new DownloadTask(newLayer, reader, progressMonitor), downloadArea); 110 } 111 112 protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) { 113 this.downloadTask = downloadTask; 114 this.currentBounds = new Bounds(downloadArea); 115 // We need submit instead of execute so we can wait for it to finish and get the error 116 // message if necessary. If no one calls getErrorMessage() it just behaves like execute. 117 return Main.worker.submit(downloadTask); 118 } 119 120 /** 121 * Loads a given URL from the OSM Server 122 * @param new_layer True if the data should be saved to a new layer 123 * @param url The URL as String 124 */ 125 public Future<?> loadUrl(boolean new_layer, String url, ProgressMonitor progressMonitor) { 126 downloadTask = new DownloadTask(new_layer, 127 new OsmServerLocationReader(url), 128 progressMonitor); 129 currentBounds = null; 130 // Extract .osm filename from URL to set the new layer name 131 extractOsmFilename("http://.*/(.*\\.osm)", url); 132 return Main.worker.submit(downloadTask); 133 } 134 135 protected final void extractOsmFilename(String pattern, String url) { 136 Matcher matcher = Pattern.compile(pattern).matcher(url); 137 newLayerName = matcher.matches() ? matcher.group(1) : null; 138 } 139 140 /* (non-Javadoc) 141 * @see org.openstreetmap.josm.actions.downloadtasks.DownloadTask#acceptsUrl(java.lang.String) 142 */ 143 @Override 144 public boolean acceptsUrl(String url) { 145 return url != null && ( 146 url.matches("http://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*")// OSM API 0.6 and XAPI 147 || url.matches("http://.*/interpreter\\?data=.*") // Overpass API 148 || url.matches("http://.*/xapi\\?.*\\[@meta\\].*") // Overpass API XAPI compatibility layer 149 || url.matches("http://.*/.*\\.osm") // Remote .osm files 150 ); 151 } 152 153 public void cancel() { 154 if (downloadTask != null) { 155 downloadTask.cancel(); 156 } 157 } 158 159 protected class DownloadTask extends PleaseWaitRunnable { 160 protected OsmServerReader reader; 161 protected DataSet dataSet; 162 protected boolean newLayer; 163 164 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { 165 super(tr("Downloading data"), progressMonitor, false); 166 this.reader = reader; 167 this.newLayer = newLayer; 168 } 169 170 protected DataSet parseDataSet() throws OsmTransferException { 171 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 172 } 173 174 @Override public void realRun() throws IOException, SAXException, OsmTransferException { 175 try { 176 if (isCanceled()) 177 return; 178 dataSet = parseDataSet(); 179 } catch(Exception e) { 180 if (isCanceled()) { 181 System.out.println(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString())); 182 return; 183 } 184 if (e instanceof OsmTransferCanceledException) { 185 setCanceled(true); 186 return; 187 } else if (e instanceof OsmTransferException) { 188 rememberException(e); 189 } else { 190 rememberException(new OsmTransferException(e)); 191 } 192 DownloadOsmTask.this.setFailed(true); 193 } 194 } 195 196 protected OsmDataLayer getEditLayer() { 197 if (!Main.isDisplayingMapView()) return null; 198 return Main.map.mapView.getEditLayer(); 199 } 200 201 protected int getNumDataLayers() { 202 int count = 0; 203 if (!Main.isDisplayingMapView()) return 0; 204 Collection<Layer> layers = Main.map.mapView.getAllLayers(); 205 for (Layer layer : layers) { 206 if (layer instanceof OsmDataLayer) { 207 count++; 208 } 209 } 210 return count; 211 } 212 213 protected OsmDataLayer getFirstDataLayer() { 214 if (!Main.isDisplayingMapView()) return null; 215 Collection<Layer> layers = Main.map.mapView.getAllLayersAsList(); 216 for (Layer layer : layers) { 217 if (layer instanceof OsmDataLayer) 218 return (OsmDataLayer) layer; 219 } 220 return null; 221 } 222 223 protected OsmDataLayer createNewLayer(String layerName) { 224 if (layerName == null || layerName.isEmpty()) { 225 layerName = OsmDataLayer.createNewName(); 226 } 227 return new OsmDataLayer(dataSet, layerName, null); 228 } 229 230 protected OsmDataLayer createNewLayer() { 231 return createNewLayer(null); 232 } 233 234 @Override protected void finish() { 235 if (isFailed() || isCanceled()) 236 return; 237 if (dataSet == null) 238 return; // user canceled download or error occurred 239 if (dataSet.allPrimitives().isEmpty()) { 240 rememberErrorMessage(tr("No data found in this area.")); 241 // need to synthesize a download bounds lest the visual indication of downloaded 242 // area doesn't work 243 dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds : new Bounds(new LatLon(0, 0)), "OpenStreetMap server")); 244 } 245 246 rememberDownloadedData(dataSet); 247 int numDataLayers = getNumDataLayers(); 248 if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) { 249 // the user explicitly wants a new layer, we don't have any layer at all 250 // or it is not clear which layer to merge to 251 // 252 targetLayer = createNewLayer(newLayerName); 253 final boolean isDisplayingMapView = Main.isDisplayingMapView(); 254 255 Main.main.addLayer(targetLayer); 256 257 // If the mapView is not there yet, we cannot calculate the bounds (see constructor of MapView). 258 // Otherwise jump to the current download. 259 if (isDisplayingMapView) { 260 computeBboxAndCenterScale(); 261 } 262 } else { 263 targetLayer = getEditLayer(); 264 if (targetLayer == null) { 265 targetLayer = getFirstDataLayer(); 266 } 267 targetLayer.mergeFrom(dataSet); 268 computeBboxAndCenterScale(); 269 targetLayer.onPostDownloadFromServer(); 270 } 271 272 suggestImageryLayers(); 273 } 274 275 protected void computeBboxAndCenterScale() { 276 BoundingXYVisitor v = new BoundingXYVisitor(); 277 if (currentBounds != null) { 278 v.visit(currentBounds); 279 } else { 280 v.computeBoundingBox(dataSet.getNodes()); 281 } 282 Main.map.mapView.recalculateCenterScale(v); 283 } 284 285 @Override protected void cancel() { 286 setCanceled(true); 287 if (reader != null) { 288 reader.cancel(); 289 } 290 } 291 292 protected void suggestImageryLayers() { 293 if (currentBounds != null) { 294 final LatLon center = currentBounds.getCenter(); 295 final Set<ImageryInfo> layers = new HashSet<ImageryInfo>(); 296 297 for (ImageryInfo i : ImageryLayerInfo.instance.getDefaultLayers()) { 298 if (i.getBounds() != null && i.getBounds().contains(center)) { 299 layers.add(i); 300 } 301 } 302 // Do not suggest layers already in use 303 layers.removeAll(ImageryLayerInfo.instance.getLayers()); 304 // For layers containing complex shapes, check that center is in one of its shapes (fix #7910) 305 for (Iterator<ImageryInfo> iti = layers.iterator(); iti.hasNext(); ) { 306 List<Shape> shapes = iti.next().getBounds().getShapes(); 307 if (shapes != null && !shapes.isEmpty()) { 308 boolean found = false; 309 for (Iterator<Shape> its = shapes.iterator(); its.hasNext() && !found; ) { 310 found = its.next().contains(center); 311 } 312 if (!found) { 313 iti.remove(); 314 } 315 } 316 } 317 318 if (layers.isEmpty()) { 319 return; 320 } 321 322 final List<String> layerNames = new ArrayList<String>(); 323 for (ImageryInfo i : layers) { 324 layerNames.add(i.getName()); 325 } 326 327 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 328 "download.suggest-imagery-layer", 329 Main.parent, 330 tr("<html>For the downloaded area, the following additional imagery layers are available: {0}" + 331 "Do you want to add those layers to the <em>Imagery</em> menu?" + 332 "<br>(If needed, you can remove those entries in the <em>Preferences</em>.)", 333 Utils.joinAsHtmlUnorderedList(layerNames)), 334 tr("Add imagery layers?"), 335 JOptionPane.YES_NO_OPTION, 336 JOptionPane.QUESTION_MESSAGE, 337 JOptionPane.YES_OPTION)) { 338 return; 339 } 340 341 ImageryLayerInfo.addLayers(layers); 342 } 343 } 344 } 345 }