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

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

fix #8039, see #10456 - fix bugs with non-downloadable layers

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