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

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

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

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