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

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

see #16850 - fix error_prone warning

  • Property svn:eol-style set to native
File size: 20.5 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.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.Objects;
14import java.util.Optional;
15import java.util.Set;
16import java.util.concurrent.Future;
17import java.util.regex.Matcher;
18import java.util.regex.Pattern;
19import java.util.stream.Stream;
20
21import org.openstreetmap.josm.data.Bounds;
22import org.openstreetmap.josm.data.DataSource;
23import org.openstreetmap.josm.data.ProjectionBounds;
24import org.openstreetmap.josm.data.ViewportData;
25import org.openstreetmap.josm.data.coor.LatLon;
26import org.openstreetmap.josm.data.osm.DataSet;
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.Relation;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
31import org.openstreetmap.josm.gui.MainApplication;
32import org.openstreetmap.josm.gui.MapFrame;
33import org.openstreetmap.josm.gui.PleaseWaitRunnable;
34import org.openstreetmap.josm.gui.io.UpdatePrimitivesTask;
35import org.openstreetmap.josm.gui.layer.OsmDataLayer;
36import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
37import org.openstreetmap.josm.gui.progress.ProgressMonitor;
38import org.openstreetmap.josm.io.BoundingBoxDownloader;
39import org.openstreetmap.josm.io.OsmServerLocationReader;
40import org.openstreetmap.josm.io.OsmServerLocationReader.OsmUrlPattern;
41import org.openstreetmap.josm.io.OsmServerReader;
42import org.openstreetmap.josm.io.OsmTransferCanceledException;
43import org.openstreetmap.josm.io.OsmTransferException;
44import org.openstreetmap.josm.tools.Logging;
45import org.openstreetmap.josm.tools.Utils;
46import org.xml.sax.SAXException;
47
48/**
49 * Open the download dialog and download the data.
50 * Run in the worker thread.
51 */
52public class DownloadOsmTask extends AbstractDownloadTask<DataSet> {
53
54 protected Bounds currentBounds;
55 protected DownloadTask downloadTask;
56
57 protected String newLayerName;
58
59 /** This allows subclasses to ignore this warning */
60 protected boolean warnAboutEmptyArea = true;
61
62 @Override
63 public String[] getPatterns() {
64 if (this.getClass() == DownloadOsmTask.class) {
65 return Arrays.stream(OsmUrlPattern.values()).map(OsmUrlPattern::pattern).toArray(String[]::new);
66 } else {
67 return super.getPatterns();
68 }
69 }
70
71 @Override
72 public String getTitle() {
73 if (this.getClass() == DownloadOsmTask.class) {
74 return tr("Download OSM");
75 } else {
76 return super.getTitle();
77 }
78 }
79
80 @Override
81 public Future<?> download(DownloadParams settings, Bounds downloadArea, ProgressMonitor progressMonitor) {
82 return download(new BoundingBoxDownloader(downloadArea), settings, downloadArea, progressMonitor);
83 }
84
85 /**
86 * Asynchronously launches the download task for a given bounding box.
87 *
88 * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor.
89 * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to
90 * be discarded.
91 *
92 * You can wait for the asynchronous download task to finish by synchronizing on the returned
93 * {@link Future}, but make sure not to freeze up JOSM. Example:
94 * <pre>
95 * Future&lt;?&gt; future = task.download(...);
96 * // DON'T run this on the Swing EDT or JOSM will freeze
97 * future.get(); // waits for the dowload task to complete
98 * </pre>
99 *
100 * The following example uses a pattern which is better suited if a task is launched from
101 * the Swing EDT:
102 * <pre>
103 * final Future&lt;?&gt; future = task.download(...);
104 * Runnable runAfterTask = new Runnable() {
105 * public void run() {
106 * // this is not strictly necessary because of the type of executor service
107 * // Main.worker is initialized with, but it doesn't harm either
108 * //
109 * future.get(); // wait for the download task to complete
110 * doSomethingAfterTheTaskCompleted();
111 * }
112 * }
113 * MainApplication.worker.submit(runAfterTask);
114 * </pre>
115 * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm})
116 * @param settings download settings
117 * @param downloadArea the area to download
118 * @param progressMonitor the progressMonitor
119 * @return the future representing the asynchronous task
120 * @since 13927
121 */
122 public Future<?> download(OsmServerReader reader, DownloadParams settings, Bounds downloadArea, ProgressMonitor progressMonitor) {
123 return download(new DownloadTask(settings, reader, progressMonitor, zoomAfterDownload), downloadArea);
124 }
125
126 protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) {
127 this.downloadTask = downloadTask;
128 this.currentBounds = new Bounds(downloadArea);
129 // We need submit instead of execute so we can wait for it to finish and get the error
130 // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
131 return MainApplication.worker.submit(downloadTask);
132 }
133
134 /**
135 * This allows subclasses to perform operations on the URL before {@link #loadUrl} is performed.
136 * @param url the original URL
137 * @return the modified URL
138 */
139 protected String modifyUrlBeforeLoad(String url) {
140 return url;
141 }
142
143 /**
144 * Loads a given URL from the OSM Server
145 * @param settings download settings
146 * @param url The URL as String
147 */
148 @Override
149 public Future<?> loadUrl(DownloadParams settings, String url, ProgressMonitor progressMonitor) {
150 String newUrl = modifyUrlBeforeLoad(url);
151 downloadTask = new DownloadTask(settings,
152 new OsmServerLocationReader(newUrl),
153 progressMonitor);
154 currentBounds = null;
155 // Extract .osm filename from URL to set the new layer name
156 extractOsmFilename(settings, "https?://.*/(.*\\.osm)", newUrl);
157 return MainApplication.worker.submit(downloadTask);
158 }
159
160 protected final void extractOsmFilename(DownloadParams settings, String pattern, String url) {
161 newLayerName = settings.getLayerName();
162 if (newLayerName == null || newLayerName.isEmpty()) {
163 Matcher matcher = Pattern.compile(pattern).matcher(url);
164 newLayerName = matcher.matches() ? matcher.group(1) : null;
165 }
166 }
167
168 @Override
169 public void cancel() {
170 if (downloadTask != null) {
171 downloadTask.cancel();
172 }
173 }
174
175 @Override
176 public boolean isSafeForRemotecontrolRequests() {
177 return true;
178 }
179
180 @Override
181 public ProjectionBounds getDownloadProjectionBounds() {
182 return downloadTask != null ? downloadTask.computeBbox(currentBounds) : null;
183 }
184
185 /**
186 * Superclass of internal download task.
187 * @since 7636
188 */
189 public abstract static class AbstractInternalTask extends PleaseWaitRunnable {
190
191 protected final DownloadParams settings;
192 protected final boolean zoomAfterDownload;
193 protected DataSet dataSet;
194
195 /**
196 * Constructs a new {@code AbstractInternalTask}.
197 * @param settings download settings
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(DownloadParams settings, String title, boolean ignoreException, boolean zoomAfterDownload) {
205 super(title, ignoreException);
206 this.settings = Objects.requireNonNull(settings);
207 this.zoomAfterDownload = zoomAfterDownload;
208 }
209
210 /**
211 * Constructs a new {@code AbstractInternalTask}.
212 * @param settings download settings
213 * @param title message for the user
214 * @param progressMonitor progress monitor
215 * @param ignoreException If true, exception will be propagated to calling code. If false then
216 * exception will be thrown directly in EDT. When this runnable is executed using executor framework
217 * then use false unless you read result of task (because exception will get lost if you don't)
218 * @param zoomAfterDownload If true, the map view will zoom to download area after download
219 */
220 public AbstractInternalTask(DownloadParams settings, String title, ProgressMonitor progressMonitor, boolean ignoreException,
221 boolean zoomAfterDownload) {
222 super(title, progressMonitor, ignoreException);
223 this.settings = Objects.requireNonNull(settings);
224 this.zoomAfterDownload = zoomAfterDownload;
225 }
226
227 protected OsmDataLayer getEditLayer() {
228 return MainApplication.getLayerManager().getEditLayer();
229 }
230
231 private static Stream<OsmDataLayer> getModifiableDataLayers() {
232 return MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class)
233 .stream().filter(OsmDataLayer::isDownloadable);
234 }
235
236 /**
237 * Returns the number of modifiable data layers
238 * @return number of modifiable data layers
239 * @since 13434
240 */
241 protected long getNumModifiableDataLayers() {
242 return getModifiableDataLayers().count();
243 }
244
245 /**
246 * Returns the first modifiable data layer
247 * @return the first modifiable data layer
248 * @since 13434
249 */
250 protected OsmDataLayer getFirstModifiableDataLayer() {
251 return getModifiableDataLayers().findFirst().orElse(null);
252 }
253
254 /**
255 * Creates a name for a new layer by utilizing the settings ({@link DownloadParams#getLayerName()}) or
256 * {@link OsmDataLayer#createNewName()} if the former option is {@code null}.
257 *
258 * @return a name for a new layer
259 * @since 14347
260 */
261 protected String generateLayerName() {
262 return Optional.ofNullable(settings.getLayerName())
263 .filter(layerName -> !Utils.isStripEmpty(layerName))
264 .orElse(OsmDataLayer.createNewName());
265 }
266
267 /**
268 * Can be overridden (e.g. by plugins) if a subclass of {@link OsmDataLayer} is needed.
269 * If you want to change how the name is determined, consider overriding
270 * {@link #generateLayerName()} instead.
271 *
272 * @param ds the dataset on which the layer is based, must be non-null
273 * @param layerName the name of the new layer, must be either non-blank or non-present
274 * @return a new instance of {@link OsmDataLayer} constructed with the given arguments
275 * @since 14347
276 */
277 protected OsmDataLayer createNewLayer(final DataSet ds, final Optional<String> layerName) {
278 if (layerName.filter(Utils::isStripEmpty).isPresent()) {
279 throw new IllegalArgumentException("Blank layer name!");
280 }
281 return new OsmDataLayer(
282 Objects.requireNonNull(ds, "dataset parameter"),
283 layerName.orElseGet(this::generateLayerName),
284 null
285 );
286 }
287
288 /**
289 * Convenience method for {@link #createNewLayer(DataSet, Optional)}, uses the dataset
290 * from field {@link #dataSet} and applies the settings from field {@link #settings}.
291 *
292 * @param layerName an optional layer name, must be non-blank if the [Optional] is present
293 * @return a newly constructed layer
294 * @since 14347
295 */
296 protected final OsmDataLayer createNewLayer(final Optional<String> layerName) {
297 Optional.ofNullable(settings.getDownloadPolicy())
298 .ifPresent(dataSet::setDownloadPolicy);
299 Optional.ofNullable(settings.getUploadPolicy())
300 .ifPresent(dataSet::setUploadPolicy);
301 if (dataSet.isLocked() && !settings.isLocked()) {
302 dataSet.unlock();
303 } else if (!dataSet.isLocked() && settings.isLocked()) {
304 dataSet.lock();
305 }
306 return createNewLayer(dataSet, layerName);
307 }
308
309 /**
310 * @param layerName the name of the new layer
311 * @deprecated Use {@link #createNewLayer(DataSet, Optional)}
312 * @return a newly constructed layer
313 */
314 @Deprecated
315 protected OsmDataLayer createNewLayer(final String layerName) {
316 return createNewLayer(Optional.ofNullable(layerName).filter(it -> !Utils.isStripEmpty(it)));
317 }
318
319 /**
320 * @deprecated Use {@link #createNewLayer(Optional)}
321 * @return a newly constructed layer
322 */
323 @Deprecated
324 protected OsmDataLayer createNewLayer() {
325 return createNewLayer(Optional.empty());
326 }
327
328 protected ProjectionBounds computeBbox(Bounds bounds) {
329 BoundingXYVisitor v = new BoundingXYVisitor();
330 if (bounds != null) {
331 v.visit(bounds);
332 } else {
333 v.computeBoundingBox(dataSet.getNodes());
334 }
335 return v.getBounds();
336 }
337
338 protected OsmDataLayer addNewLayerIfRequired(String newLayerName) {
339 long numDataLayers = getNumModifiableDataLayers();
340 if (settings.isNewLayer() || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
341 // the user explicitly wants a new layer, we don't have any layer at all
342 // or it is not clear which layer to merge to
343 final OsmDataLayer layer = createNewLayer(newLayerName);
344 MainApplication.getLayerManager().addLayer(layer, zoomAfterDownload);
345 return layer;
346 }
347 return null;
348 }
349
350 protected void loadData(String newLayerName, Bounds bounds) {
351 OsmDataLayer layer = addNewLayerIfRequired(newLayerName);
352 if (layer == null) {
353 layer = getEditLayer();
354 if (layer == null || !layer.isDownloadable()) {
355 layer = getFirstModifiableDataLayer();
356 }
357 Collection<OsmPrimitive> primitivesToUpdate = searchPrimitivesToUpdate(bounds, layer.getDataSet());
358 layer.mergeFrom(dataSet);
359 MapFrame map = MainApplication.getMap();
360 if (map != null && zoomAfterDownload && bounds != null) {
361 map.mapView.zoomTo(new ViewportData(computeBbox(bounds)));
362 }
363 if (!primitivesToUpdate.isEmpty()) {
364 MainApplication.worker.submit(new UpdatePrimitivesTask(layer, primitivesToUpdate));
365 }
366 layer.onPostDownloadFromServer();
367 }
368 }
369
370 /**
371 * Look for primitives deleted on server (thus absent from downloaded data)
372 * but still present in existing data layer
373 * @param bounds download bounds
374 * @param ds existing data set
375 * @return the primitives to update
376 */
377 private Collection<OsmPrimitive> searchPrimitivesToUpdate(Bounds bounds, DataSet ds) {
378 if (bounds == null)
379 return Collections.emptySet();
380 Collection<OsmPrimitive> col = new ArrayList<>();
381 ds.searchNodes(bounds.toBBox()).stream().filter(n -> !n.isNew() && !dataSet.containsNode(n)).forEachOrdered(col::add);
382 if (!col.isEmpty()) {
383 Set<Way> ways = new HashSet<>();
384 Set<Relation> rels = new HashSet<>();
385 for (OsmPrimitive n : col) {
386 for (OsmPrimitive ref : n.getReferrers()) {
387 if (ref.isNew()) {
388 continue;
389 } else if (ref instanceof Way) {
390 ways.add((Way) ref);
391 } else if (ref instanceof Relation) {
392 rels.add((Relation) ref);
393 }
394 }
395 }
396 ways.stream().filter(w -> !dataSet.containsWay(w)).forEachOrdered(col::add);
397 rels.stream().filter(r -> !dataSet.containsRelation(r)).forEachOrdered(col::add);
398 }
399 return col;
400 }
401 }
402
403 protected class DownloadTask extends AbstractInternalTask {
404 protected final OsmServerReader reader;
405
406 /**
407 * Constructs a new {@code DownloadTask}.
408 * @param settings download settings
409 * @param reader OSM data reader
410 * @param progressMonitor progress monitor
411 * @since 13927
412 */
413 public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor) {
414 this(settings, reader, progressMonitor, true);
415 }
416
417 /**
418 * Constructs a new {@code DownloadTask}.
419 * @param settings download settings
420 * @param reader OSM data reader
421 * @param progressMonitor progress monitor
422 * @param zoomAfterDownload If true, the map view will zoom to download area after download
423 * @since 13927
424 */
425 public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) {
426 super(settings, tr("Downloading data"), progressMonitor, false, zoomAfterDownload);
427 this.reader = reader;
428 }
429
430 protected DataSet parseDataSet() throws OsmTransferException {
431 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
432 }
433
434 @Override
435 public void realRun() throws IOException, SAXException, OsmTransferException {
436 try {
437 if (isCanceled())
438 return;
439 dataSet = parseDataSet();
440 } catch (OsmTransferException e) {
441 if (isCanceled()) {
442 Logging.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
443 return;
444 }
445 if (e instanceof OsmTransferCanceledException) {
446 setCanceled(true);
447 return;
448 } else {
449 rememberException(e);
450 }
451 DownloadOsmTask.this.setFailed(true);
452 }
453 }
454
455 @Override
456 protected void finish() {
457 if (isFailed() || isCanceled())
458 return;
459 if (dataSet == null)
460 return; // user canceled download or error occurred
461 if (dataSet.allPrimitives().isEmpty()) {
462 if (warnAboutEmptyArea) {
463 rememberErrorMessage(tr("No data found in this area."));
464 }
465 String remark = dataSet.getRemark();
466 if (remark != null && !remark.isEmpty()) {
467 rememberErrorMessage(remark);
468 }
469 // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work
470 dataSet.addDataSource(new DataSource(currentBounds != null ? currentBounds :
471 new Bounds(LatLon.ZERO), "OpenStreetMap server"));
472 }
473
474 rememberDownloadedData(dataSet);
475 loadData(newLayerName, currentBounds);
476 }
477
478 @Override
479 protected void cancel() {
480 setCanceled(true);
481 if (reader != null) {
482 reader.cancel();
483 }
484 }
485 }
486
487 @Override
488 public String getConfirmationMessage(URL url) {
489 if (url != null) {
490 String urlString = url.toExternalForm();
491 if (urlString.matches(OsmUrlPattern.OSM_API_URL.pattern())) {
492 // TODO: proper i18n after stabilization
493 Collection<String> items = new ArrayList<>();
494 items.add(tr("OSM Server URL:") + ' ' + url.getHost());
495 items.add(tr("Command")+": "+url.getPath());
496 if (url.getQuery() != null) {
497 items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")));
498 }
499 return Utils.joinAsHtmlUnorderedList(items);
500 }
501 // TODO: other APIs
502 }
503 return null;
504 }
505}
Note: See TracBrowser for help on using the repository browser.