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

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

fix various SonarQube issues

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