source: josm/trunk/src/org/openstreetmap/josm/actions/downloadtasks/DownloadOsmTask.java@ 9732

Last change on this file since 9732 was 9732, checked in by Don-vip, 8 years ago

remote control: add more unit tests, robustness

  • Property svn:eol-style set to native
File size: 15.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.downloadtasks;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.net.URL;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.concurrent.Future;
11import java.util.regex.Matcher;
12import java.util.regex.Pattern;
13
14import org.openstreetmap.josm.Main;
15import org.openstreetmap.josm.data.Bounds;
16import org.openstreetmap.josm.data.DataSource;
17import org.openstreetmap.josm.data.ProjectionBounds;
18import org.openstreetmap.josm.data.coor.LatLon;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
21import org.openstreetmap.josm.gui.PleaseWaitRunnable;
22import org.openstreetmap.josm.gui.layer.Layer;
23import org.openstreetmap.josm.gui.layer.OsmDataLayer;
24import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
25import org.openstreetmap.josm.gui.progress.ProgressMonitor;
26import org.openstreetmap.josm.io.BoundingBoxDownloader;
27import org.openstreetmap.josm.io.OsmServerLocationReader;
28import org.openstreetmap.josm.io.OsmServerReader;
29import org.openstreetmap.josm.io.OsmTransferCanceledException;
30import org.openstreetmap.josm.io.OsmTransferException;
31import org.openstreetmap.josm.tools.Utils;
32import org.xml.sax.SAXException;
33
34/**
35 * Open the download dialog and download the data.
36 * Run in the worker thread.
37 */
38public class DownloadOsmTask extends AbstractDownloadTask<DataSet> {
39
40 protected static final String PATTERN_OSM_API_URL = "https?://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*";
41 protected static final String PATTERN_OVERPASS_API_URL = "https?://.*/interpreter\\?data=.*";
42 protected static final String PATTERN_OVERPASS_API_XAPI_URL = "https?://.*/xapi(\\?.*\\[@meta\\]|_meta\\?).*";
43 protected static final String PATTERN_EXTERNAL_OSM_FILE = "https?://.*/.*\\.osm";
44
45 protected Bounds currentBounds;
46 protected DownloadTask downloadTask;
47
48 protected String newLayerName;
49
50 /** This allows subclasses to ignore this warning */
51 protected boolean warnAboutEmptyArea = true;
52
53 @Override
54 public String[] getPatterns() {
55 if (this.getClass() == DownloadOsmTask.class) {
56 return new String[]{PATTERN_OSM_API_URL, PATTERN_OVERPASS_API_URL,
57 PATTERN_OVERPASS_API_XAPI_URL, PATTERN_EXTERNAL_OSM_FILE};
58 } else {
59 return super.getPatterns();
60 }
61 }
62
63 @Override
64 public String getTitle() {
65 if (this.getClass() == DownloadOsmTask.class) {
66 return tr("Download OSM");
67 } else {
68 return super.getTitle();
69 }
70 }
71
72 @Override
73 public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
74 return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor);
75 }
76
77 /**
78 * Asynchronously launches the download task for a given bounding box.
79 *
80 * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor.
81 * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to
82 * be discarded.
83 *
84 * You can wait for the asynchronous download task to finish by synchronizing on the returned
85 * {@link Future}, but make sure not to freeze up JOSM. Example:
86 * <pre>
87 * Future&lt;?&gt; future = task.download(...);
88 * // DON'T run this on the Swing EDT or JOSM will freeze
89 * future.get(); // waits for the dowload task to complete
90 * </pre>
91 *
92 * The following example uses a pattern which is better suited if a task is launched from
93 * the Swing EDT:
94 * <pre>
95 * final Future&lt;?&gt; future = task.download(...);
96 * Runnable runAfterTask = new Runnable() {
97 * public void run() {
98 * // this is not strictly necessary because of the type of executor service
99 * // Main.worker is initialized with, but it doesn't harm either
100 * //
101 * future.get(); // wait for the download task to complete
102 * doSomethingAfterTheTaskCompleted();
103 * }
104 * }
105 * Main.worker.submit(runAfterTask);
106 * </pre>
107 * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm})
108 * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task
109 * selects one of the existing layers as download layer, preferably the active layer.
110 * @param downloadArea the area to download
111 * @param progressMonitor the progressMonitor
112 * @return the future representing the asynchronous task
113 */
114 public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
115 return download(new DownloadTask(newLayer, reader, progressMonitor), downloadArea);
116 }
117
118 protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) {
119 this.downloadTask = downloadTask;
120 this.currentBounds = new Bounds(downloadArea);
121 // We need submit instead of execute so we can wait for it to finish and get the error
122 // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
123 return Main.worker.submit(downloadTask);
124 }
125
126 /**
127 * This allows subclasses to perform operations on the URL before {@link #loadUrl} is performed.
128 * @param url the original URL
129 * @return the modified URL
130 */
131 protected String modifyUrlBeforeLoad(String url) {
132 return url;
133 }
134
135 /**
136 * Loads a given URL from the OSM Server
137 * @param newLayer True if the data should be saved to a new layer
138 * @param url The URL as String
139 */
140 @Override
141 public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) {
142 String newUrl = modifyUrlBeforeLoad(url);
143 downloadTask = new DownloadTask(newLayer,
144 new OsmServerLocationReader(newUrl),
145 progressMonitor);
146 currentBounds = null;
147 // Extract .osm filename from URL to set the new layer name
148 extractOsmFilename("https?://.*/(.*\\.osm)", newUrl);
149 return Main.worker.submit(downloadTask);
150 }
151
152 protected final void extractOsmFilename(String pattern, String url) {
153 Matcher matcher = Pattern.compile(pattern).matcher(url);
154 newLayerName = matcher.matches() ? matcher.group(1) : null;
155 }
156
157 @Override
158 public void cancel() {
159 if (downloadTask != null) {
160 downloadTask.cancel();
161 }
162 }
163
164 @Override
165 public boolean isSafeForRemotecontrolRequests() {
166 return true;
167 }
168
169 /**
170 * Superclass of internal download task.
171 * @since 7636
172 */
173 public abstract static class AbstractInternalTask extends PleaseWaitRunnable {
174
175 protected final boolean newLayer;
176 protected final boolean zoomAfterDownload;
177 protected DataSet dataSet;
178
179 /**
180 * Constructs a new {@code AbstractInternalTask}.
181 *
182 * @param newLayer if {@code true}, force download to a new layer
183 * @param title message for the user
184 * @param ignoreException If true, exception will be propagated to calling code. If false then
185 * exception will be thrown directly in EDT. When this runnable is executed using executor framework
186 * then use false unless you read result of task (because exception will get lost if you don't)
187 * @param zoomAfterDownload If true, the map view will zoom to download area after download
188 */
189 public AbstractInternalTask(boolean newLayer, String title, boolean ignoreException, boolean zoomAfterDownload) {
190 super(title, ignoreException);
191 this.newLayer = newLayer;
192 this.zoomAfterDownload = zoomAfterDownload;
193 }
194
195 /**
196 * Constructs a new {@code AbstractInternalTask}.
197 *
198 * @param newLayer if {@code true}, force download to a new layer
199 * @param title message for the user
200 * @param progressMonitor progress monitor
201 * @param ignoreException If true, exception will be propagated to calling code. If false then
202 * exception will be thrown directly in EDT. When this runnable is executed using executor framework
203 * then use false unless you read result of task (because exception will get lost if you don't)
204 * @param zoomAfterDownload If true, the map view will zoom to download area after download
205 */
206 public AbstractInternalTask(boolean newLayer, String title, ProgressMonitor progressMonitor, boolean ignoreException,
207 boolean zoomAfterDownload) {
208 super(title, progressMonitor, ignoreException);
209 this.newLayer = newLayer;
210 this.zoomAfterDownload = zoomAfterDownload;
211 }
212
213 protected OsmDataLayer getEditLayer() {
214 if (!Main.isDisplayingMapView()) return null;
215 return Main.main.getEditLayer();
216 }
217
218 protected int getNumDataLayers() {
219 if (!Main.isDisplayingMapView()) return 0;
220 int count = 0;
221 Collection<Layer> layers = Main.map.mapView.getAllLayers();
222 for (Layer layer : layers) {
223 if (layer instanceof OsmDataLayer) {
224 count++;
225 }
226 }
227 return count;
228 }
229
230 protected OsmDataLayer getFirstDataLayer() {
231 if (!Main.isDisplayingMapView()) return null;
232 Collection<Layer> layers = Main.map.mapView.getAllLayersAsList();
233 for (Layer layer : layers) {
234 if (layer instanceof OsmDataLayer)
235 return (OsmDataLayer) layer;
236 }
237 return null;
238 }
239
240 protected OsmDataLayer createNewLayer(String layerName) {
241 if (layerName == null || layerName.isEmpty()) {
242 layerName = OsmDataLayer.createNewName();
243 }
244 return new OsmDataLayer(dataSet, layerName, null);
245 }
246
247 protected OsmDataLayer createNewLayer() {
248 return createNewLayer(null);
249 }
250
251 protected ProjectionBounds computeBbox(Bounds bounds) {
252 BoundingXYVisitor v = new BoundingXYVisitor();
253 if (bounds != null) {
254 v.visit(bounds);
255 } else {
256 v.computeBoundingBox(dataSet.getNodes());
257 }
258 return v.getBounds();
259 }
260
261 protected void computeBboxAndCenterScale(Bounds bounds) {
262 ProjectionBounds pb = computeBbox(bounds);
263 BoundingXYVisitor v = new BoundingXYVisitor();
264 v.visit(pb);
265 Main.map.mapView.zoomTo(v);
266 }
267
268 protected OsmDataLayer addNewLayerIfRequired(String newLayerName, Bounds bounds) {
269 int numDataLayers = getNumDataLayers();
270 if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
271 // the user explicitly wants a new layer, we don't have any layer at all
272 // or it is not clear which layer to merge to
273 //
274 final OsmDataLayer layer = createNewLayer(newLayerName);
275 if (Main.main != null)
276 Main.main.addLayer(layer, computeBbox(bounds));
277 return layer;
278 }
279 return null;
280 }
281
282 protected void loadData(String newLayerName, Bounds bounds) {
283 OsmDataLayer layer = addNewLayerIfRequired(newLayerName, bounds);
284 if (layer == null) {
285 layer = getEditLayer();
286 if (layer == null) {
287 layer = getFirstDataLayer();
288 }
289 layer.mergeFrom(dataSet);
290 if (zoomAfterDownload) {
291 computeBboxAndCenterScale(bounds);
292 }
293 layer.onPostDownloadFromServer();
294 }
295 }
296 }
297
298 protected class DownloadTask extends AbstractInternalTask {
299 protected final OsmServerReader reader;
300
301 /**
302 * Constructs a new {@code DownloadTask}.
303 * @param newLayer if {@code true}, force download to a new layer
304 * @param reader OSM data reader
305 * @param progressMonitor progress monitor
306 */
307 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
308 this(newLayer, reader, progressMonitor, true);
309 }
310
311 /**
312 * Constructs a new {@code DownloadTask}.
313 * @param newLayer if {@code true}, force download to a new layer
314 * @param reader OSM data reader
315 * @param progressMonitor progress monitor
316 * @param zoomAfterDownload If true, the map view will zoom to download area after download
317 * @since 8942
318 */
319 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) {
320 super(newLayer, tr("Downloading data"), progressMonitor, false, zoomAfterDownload);
321 this.reader = reader;
322 }
323
324 protected DataSet parseDataSet() throws OsmTransferException {
325 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
326 }
327
328 @Override
329 public void realRun() throws IOException, SAXException, OsmTransferException {
330 try {
331 if (isCanceled())
332 return;
333 dataSet = parseDataSet();
334 } catch (Exception e) {
335 if (isCanceled()) {
336 Main.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
337 return;
338 }
339 if (e instanceof OsmTransferCanceledException) {
340 setCanceled(true);
341 return;
342 } else if (e instanceof OsmTransferException) {
343 rememberException(e);
344 } else {
345 rememberException(new OsmTransferException(e));
346 }
347 DownloadOsmTask.this.setFailed(true);
348 }
349 }
350
351 @Override
352 protected void finish() {
353 if (isFailed() || isCanceled())
354 return;
355 if (dataSet == null)
356 return; // user canceled download or error occurred
357 if (dataSet.allPrimitives().isEmpty()) {
358 if (warnAboutEmptyArea) {
359 rememberErrorMessage(tr("No data found in this area."));
360 }
361 // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work
362 dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds :
363 new Bounds(LatLon.ZERO), "OpenStreetMap server"));
364 }
365
366 rememberDownloadedData(dataSet);
367 loadData(newLayerName, currentBounds);
368 }
369
370 @Override
371 protected void cancel() {
372 setCanceled(true);
373 if (reader != null) {
374 reader.cancel();
375 }
376 }
377 }
378
379 @Override
380 public String getConfirmationMessage(URL url) {
381 if (url != null) {
382 String urlString = url.toExternalForm();
383 if (urlString.matches(PATTERN_OSM_API_URL)) {
384 // TODO: proper i18n after stabilization
385 Collection<String> items = new ArrayList<>();
386 items.add(tr("OSM Server URL:") + ' ' + url.getHost());
387 items.add(tr("Command")+": "+url.getPath());
388 if (url.getQuery() != null) {
389 items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")));
390 }
391 return Utils.joinAsHtmlUnorderedList(items);
392 }
393 // TODO: other APIs
394 }
395 return null;
396 }
397}
Note: See TracBrowser for help on using the repository browser.