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

Last change on this file since 14374 was 14374, checked in by simon04, 5 months ago

fix #16879, see #14831 - Fix zoom to downloaded data

  • Property svn:eol-style set to native
File size: 21.5 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
[14347]329        /**
330         * @param layerName the name of the new layer
331         * @deprecated Use {@link #createNewLayer(DataSet, Optional)}
332         * @return a newly constructed layer
333         */
334        @Deprecated
335        protected OsmDataLayer createNewLayer(final String layerName) {
336            return createNewLayer(Optional.ofNullable(layerName).filter(it -> !Utils.isStripEmpty(it)));
337        }
338
339        /**
340         * @deprecated Use {@link #createNewLayer(Optional)}
341         * @return a newly constructed layer
342         */
343        @Deprecated
[4523]344        protected OsmDataLayer createNewLayer() {
[14347]345            return createNewLayer(Optional.empty());
[4523]346        }
[1869]347
[14374]348        protected Optional<ProjectionBounds> computeBbox(Bounds bounds) {
[7636]349            BoundingXYVisitor v = new BoundingXYVisitor();
350            if (bounds != null) {
351                v.visit(bounds);
352            } else {
353                v.computeBoundingBox(dataSet.getNodes());
[1082]354            }
[14374]355            return Optional.ofNullable(v.getBounds());
[7816]356        }
357
[10409]358        protected OsmDataLayer addNewLayerIfRequired(String newLayerName) {
[13434]359            long numDataLayers = getNumModifiableDataLayers();
[13928]360            if (settings.isNewLayer() || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
[1869]361                // the user explicitly wants a new layer, we don't have any layer at all
362                // or it is not clear which layer to merge to
[7636]363                final OsmDataLayer layer = createNewLayer(newLayerName);
[12636]364                MainApplication.getLayerManager().addLayer(layer, zoomAfterDownload);
[7636]365                return layer;
366            }
367            return null;
368        }
369
370        protected void loadData(String newLayerName, Bounds bounds) {
[10409]371            OsmDataLayer layer = addNewLayerIfRequired(newLayerName);
[7636]372            if (layer == null) {
[13486]373                layer = getEditLayer();
374                if (layer == null || !layer.isDownloadable()) {
375                    layer = getFirstModifiableDataLayer();
376                }
[13612]377                Collection<OsmPrimitive> primitivesToUpdate = searchPrimitivesToUpdate(bounds, layer.getDataSet());
[7636]378                layer.mergeFrom(dataSet);
[12630]379                MapFrame map = MainApplication.getMap();
[14374]380                if (map != null && zoomAfterDownload) {
381                    computeBbox(bounds).map(ViewportData::new).ifPresent(map.mapView::zoomTo);
[8927]382                }
[11735]383                if (!primitivesToUpdate.isEmpty()) {
[12634]384                    MainApplication.worker.submit(new UpdatePrimitivesTask(layer, primitivesToUpdate));
[11735]385                }
[7636]386                layer.onPostDownloadFromServer();
[1670]387            }
[1169]388        }
[11735]389
390        /**
391         * Look for primitives deleted on server (thus absent from downloaded data)
392         * but still present in existing data layer
[11836]393         * @param bounds download bounds
[11735]394         * @param ds existing data set
395         * @return the primitives to update
396         */
[11836]397        private Collection<OsmPrimitive> searchPrimitivesToUpdate(Bounds bounds, DataSet ds) {
398            if (bounds == null)
[11849]399                return Collections.emptySet();
[11735]400            Collection<OsmPrimitive> col = new ArrayList<>();
[11836]401            ds.searchNodes(bounds.toBBox()).stream().filter(n -> !n.isNew() && !dataSet.containsNode(n)).forEachOrdered(col::add);
[11735]402            if (!col.isEmpty()) {
403                Set<Way> ways = new HashSet<>();
404                Set<Relation> rels = new HashSet<>();
405                for (OsmPrimitive n : col) {
406                    for (OsmPrimitive ref : n.getReferrers()) {
407                        if (ref.isNew()) {
408                            continue;
409                        } else if (ref instanceof Way) {
410                            ways.add((Way) ref);
411                        } else if (ref instanceof Relation) {
412                            rels.add((Relation) ref);
413                        }
414                    }
415                }
416                ways.stream().filter(w -> !dataSet.containsWay(w)).forEachOrdered(col::add);
417                rels.stream().filter(r -> !dataSet.containsRelation(r)).forEachOrdered(col::add);
418            }
419            return col;
420        }
[7636]421    }
[6069]422
[7636]423    protected class DownloadTask extends AbstractInternalTask {
424        protected final OsmServerReader reader;
425
[8927]426        /**
427         * Constructs a new {@code DownloadTask}.
[13927]428         * @param settings download settings
[8927]429         * @param reader OSM data reader
430         * @param progressMonitor progress monitor
[13927]431         * @since 13927
[8927]432         */
[13927]433        public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor) {
434            this(settings, reader, progressMonitor, true);
[8942]435        }
436
437        /**
438         * Constructs a new {@code DownloadTask}.
[13927]439         * @param settings download settings
[8942]440         * @param reader OSM data reader
441         * @param progressMonitor progress monitor
442         * @param zoomAfterDownload If true, the map view will zoom to download area after download
[13927]443         * @since 13927
[8942]444         */
[13927]445        public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) {
446            super(settings, tr("Downloading data"), progressMonitor, false, zoomAfterDownload);
[7636]447            this.reader = reader;
448        }
449
450        protected DataSet parseDataSet() throws OsmTransferException {
451            return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
452        }
453
454        @Override
455        public void realRun() throws IOException, SAXException, OsmTransferException {
456            try {
457                if (isCanceled())
458                    return;
459                dataSet = parseDataSet();
[10212]460            } catch (OsmTransferException e) {
[7636]461                if (isCanceled()) {
[12620]462                    Logging.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
[7636]463                    return;
464                }
465                if (e instanceof OsmTransferCanceledException) {
466                    setCanceled(true);
467                    return;
[10212]468                } else {
[7636]469                    rememberException(e);
470                }
471                DownloadOsmTask.this.setFailed(true);
[4530]472            }
473        }
[175]474
[7636]475        @Override
476        protected void finish() {
477            if (isFailed() || isCanceled())
478                return;
479            if (dataSet == null)
480                return; // user canceled download or error occurred
481            if (dataSet.allPrimitives().isEmpty()) {
[8927]482                if (warnAboutEmptyArea) {
483                    rememberErrorMessage(tr("No data found in this area."));
484                }
[14219]485                String remark = dataSet.getRemark();
486                if (remark != null && !remark.isEmpty()) {
487                    rememberErrorMessage(remark);
488                }
[8540]489                // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work
[11627]490                dataSet.addDataSource(new DataSource(currentBounds != null ? currentBounds :
[9214]491                    new Bounds(LatLon.ZERO), "OpenStreetMap server"));
[7636]492            }
493
494            rememberDownloadedData(dataSet);
495            loadData(newLayerName, currentBounds);
496        }
497
498        @Override
499        protected void cancel() {
[2322]500            setCanceled(true);
[1670]501            if (reader != null) {
[1169]502                reader.cancel();
[1670]503            }
[1169]504        }
505    }
[5691]506
507    @Override
508    public String getConfirmationMessage(URL url) {
509        if (url != null) {
510            String urlString = url.toExternalForm();
[12679]511            if (urlString.matches(OsmUrlPattern.OSM_API_URL.pattern())) {
[5691]512                // TODO: proper i18n after stabilization
[7005]513                Collection<String> items = new ArrayList<>();
[8846]514                items.add(tr("OSM Server URL:") + ' ' + url.getHost());
[6524]515                items.add(tr("Command")+": "+url.getPath());
[5691]516                if (url.getQuery() != null) {
[6524]517                    items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")));
[5691]518                }
[6524]519                return Utils.joinAsHtmlUnorderedList(items);
[5691]520            }
521            // TODO: other APIs
522        }
523        return null;
524    }
[175]525}
Note: See TracBrowser for help on using the repository browser.