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

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

see #14604 - sonar - squid:S1596 - "Collections.EMPTY_LIST", "EMPTY_MAP", and "EMPTY_SET" should not be used

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