001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.downloadtasks;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.net.URL;
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.concurrent.Future;
011import java.util.regex.Matcher;
012import java.util.regex.Pattern;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.data.Bounds;
016import org.openstreetmap.josm.data.DataSource;
017import org.openstreetmap.josm.data.ProjectionBounds;
018import org.openstreetmap.josm.data.coor.LatLon;
019import org.openstreetmap.josm.data.osm.DataSet;
020import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
021import org.openstreetmap.josm.gui.PleaseWaitRunnable;
022import org.openstreetmap.josm.gui.layer.Layer;
023import org.openstreetmap.josm.gui.layer.OsmDataLayer;
024import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
025import org.openstreetmap.josm.gui.progress.ProgressMonitor;
026import org.openstreetmap.josm.io.BoundingBoxDownloader;
027import org.openstreetmap.josm.io.OsmServerLocationReader;
028import org.openstreetmap.josm.io.OsmServerReader;
029import org.openstreetmap.josm.io.OsmTransferCanceledException;
030import org.openstreetmap.josm.io.OsmTransferException;
031import org.openstreetmap.josm.tools.Utils;
032import org.xml.sax.SAXException;
033
034/**
035 * Open the download dialog and download the data.
036 * Run in the worker thread.
037 */
038public class DownloadOsmTask extends AbstractDownloadTask<DataSet> {
039
040    protected static final String PATTERN_OSM_API_URL           = "https?://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*";
041    protected static final String PATTERN_OVERPASS_API_URL      = "https?://.*/interpreter\\?data=.*";
042    protected static final String PATTERN_OVERPASS_API_XAPI_URL = "https?://.*/xapi(\\?.*\\[@meta\\]|_meta\\?).*";
043    protected static final String PATTERN_EXTERNAL_OSM_FILE     = "https?://.*/.*\\.osm";
044
045    protected Bounds currentBounds;
046    protected DownloadTask downloadTask;
047
048    protected String newLayerName;
049
050    /** This allows subclasses to ignore this warning */
051    protected boolean warnAboutEmptyArea = true;
052
053    @Override
054    public String[] getPatterns() {
055        if (this.getClass() == DownloadOsmTask.class) {
056            return new String[]{PATTERN_OSM_API_URL, PATTERN_OVERPASS_API_URL,
057                PATTERN_OVERPASS_API_XAPI_URL, PATTERN_EXTERNAL_OSM_FILE};
058        } else {
059            return super.getPatterns();
060        }
061    }
062
063    @Override
064    public String getTitle() {
065        if (this.getClass() == DownloadOsmTask.class) {
066            return tr("Download OSM");
067        } else {
068            return super.getTitle();
069        }
070    }
071
072    @Override
073    public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
074        return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor);
075    }
076
077    /**
078     * Asynchronously launches the download task for a given bounding box.
079     *
080     * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor.
081     * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to
082     * be discarded.
083     *
084     * You can wait for the asynchronous download task to finish by synchronizing on the returned
085     * {@link Future}, but make sure not to freeze up JOSM. Example:
086     * <pre>
087     *    Future&lt;?&gt; future = task.download(...);
088     *    // DON'T run this on the Swing EDT or JOSM will freeze
089     *    future.get(); // waits for the dowload task to complete
090     * </pre>
091     *
092     * The following example uses a pattern which is better suited if a task is launched from
093     * the Swing EDT:
094     * <pre>
095     *    final Future&lt;?&gt; future = task.download(...);
096     *    Runnable runAfterTask = new Runnable() {
097     *       public void run() {
098     *           // this is not strictly necessary because of the type of executor service
099     *           // Main.worker is initialized with, but it doesn't harm either
100     *           //
101     *           future.get(); // wait for the download task to complete
102     *           doSomethingAfterTheTaskCompleted();
103     *       }
104     *    }
105     *    Main.worker.submit(runAfterTask);
106     * </pre>
107     * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm})
108     * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task
109     *                 selects one of the existing layers as download layer, preferably the active layer.
110     * @param downloadArea the area to download
111     * @param progressMonitor the progressMonitor
112     * @return the future representing the asynchronous task
113     */
114    public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
115        return download(new DownloadTask(newLayer, reader, progressMonitor), downloadArea);
116    }
117
118    protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) {
119        this.downloadTask = downloadTask;
120        this.currentBounds = new Bounds(downloadArea);
121        // We need submit instead of execute so we can wait for it to finish and get the error
122        // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
123        return Main.worker.submit(downloadTask);
124    }
125
126    /**
127     * This allows subclasses to perform operations on the URL before {@link #loadUrl} is performed.
128     * @param url the original URL
129     * @return the modified URL
130     */
131    protected String modifyUrlBeforeLoad(String url) {
132        return url;
133    }
134
135    /**
136     * Loads a given URL from the OSM Server
137     * @param newLayer True if the data should be saved to a new layer
138     * @param url The URL as String
139     */
140    @Override
141    public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) {
142        String newUrl = modifyUrlBeforeLoad(url);
143        downloadTask = new DownloadTask(newLayer,
144                new OsmServerLocationReader(newUrl),
145                progressMonitor);
146        currentBounds = null;
147        // Extract .osm filename from URL to set the new layer name
148        extractOsmFilename("https?://.*/(.*\\.osm)", newUrl);
149        return Main.worker.submit(downloadTask);
150    }
151
152    protected final void extractOsmFilename(String pattern, String url) {
153        Matcher matcher = Pattern.compile(pattern).matcher(url);
154        newLayerName = matcher.matches() ? matcher.group(1) : null;
155    }
156
157    @Override
158    public void cancel() {
159        if (downloadTask != null) {
160            downloadTask.cancel();
161        }
162    }
163
164    @Override
165    public boolean isSafeForRemotecontrolRequests() {
166        return true;
167    }
168
169    /**
170     * Superclass of internal download task.
171     * @since 7636
172     */
173    public abstract static class AbstractInternalTask extends PleaseWaitRunnable {
174
175        protected final boolean newLayer;
176        protected final boolean zoomAfterDownload;
177        protected DataSet dataSet;
178
179        /**
180         * Constructs a new {@code AbstractInternalTask}.
181         *
182         * @param newLayer if {@code true}, force download to a new layer
183         * @param title message for the user
184         * @param ignoreException If true, exception will be propagated to calling code. If false then
185         * exception will be thrown directly in EDT. When this runnable is executed using executor framework
186         * then use false unless you read result of task (because exception will get lost if you don't)
187         * @param zoomAfterDownload If true, the map view will zoom to download area after download
188         */
189        public AbstractInternalTask(boolean newLayer, String title, boolean ignoreException, boolean zoomAfterDownload) {
190            super(title, ignoreException);
191            this.newLayer = newLayer;
192            this.zoomAfterDownload = zoomAfterDownload;
193        }
194
195        /**
196         * Constructs a new {@code AbstractInternalTask}.
197         *
198         * @param newLayer if {@code true}, force download to a new layer
199         * @param title message for the user
200         * @param progressMonitor progress monitor
201         * @param ignoreException If true, exception will be propagated to calling code. If false then
202         * exception will be thrown directly in EDT. When this runnable is executed using executor framework
203         * then use false unless you read result of task (because exception will get lost if you don't)
204         * @param zoomAfterDownload If true, the map view will zoom to download area after download
205         */
206        public AbstractInternalTask(boolean newLayer, String title, ProgressMonitor progressMonitor, boolean ignoreException,
207                boolean zoomAfterDownload) {
208            super(title, progressMonitor, ignoreException);
209            this.newLayer = newLayer;
210            this.zoomAfterDownload = zoomAfterDownload;
211        }
212
213        protected OsmDataLayer getEditLayer() {
214            if (!Main.isDisplayingMapView()) return null;
215            return Main.main.getEditLayer();
216        }
217
218        protected int getNumDataLayers() {
219            if (!Main.isDisplayingMapView()) return 0;
220            int count = 0;
221            Collection<Layer> layers = Main.map.mapView.getAllLayers();
222            for (Layer layer : layers) {
223                if (layer instanceof OsmDataLayer) {
224                    count++;
225                }
226            }
227            return count;
228        }
229
230        protected OsmDataLayer getFirstDataLayer() {
231            if (!Main.isDisplayingMapView()) return null;
232            Collection<Layer> layers = Main.map.mapView.getAllLayersAsList();
233            for (Layer layer : layers) {
234                if (layer instanceof OsmDataLayer)
235                    return (OsmDataLayer) layer;
236            }
237            return null;
238        }
239
240        protected OsmDataLayer createNewLayer(String layerName) {
241            if (layerName == null || layerName.isEmpty()) {
242                layerName = OsmDataLayer.createNewName();
243            }
244            return new OsmDataLayer(dataSet, layerName, null);
245        }
246
247        protected OsmDataLayer createNewLayer() {
248            return createNewLayer(null);
249        }
250
251        protected ProjectionBounds computeBbox(Bounds bounds) {
252            BoundingXYVisitor v = new BoundingXYVisitor();
253            if (bounds != null) {
254                v.visit(bounds);
255            } else {
256                v.computeBoundingBox(dataSet.getNodes());
257            }
258            return v.getBounds();
259        }
260
261        protected void computeBboxAndCenterScale(Bounds bounds) {
262            ProjectionBounds pb = computeBbox(bounds);
263            BoundingXYVisitor v = new BoundingXYVisitor();
264            v.visit(pb);
265            Main.map.mapView.zoomTo(v);
266        }
267
268        protected OsmDataLayer addNewLayerIfRequired(String newLayerName, Bounds bounds) {
269            int numDataLayers = getNumDataLayers();
270            if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
271                // the user explicitly wants a new layer, we don't have any layer at all
272                // or it is not clear which layer to merge to
273                //
274                final OsmDataLayer layer = createNewLayer(newLayerName);
275                Main.main.addLayer(layer, computeBbox(bounds));
276                return layer;
277            }
278            return null;
279        }
280
281        protected void loadData(String newLayerName, Bounds bounds) {
282            OsmDataLayer layer = addNewLayerIfRequired(newLayerName, bounds);
283            if (layer == null) {
284                layer = getEditLayer();
285                if (layer == null) {
286                    layer = getFirstDataLayer();
287                }
288                layer.mergeFrom(dataSet);
289                if (zoomAfterDownload) {
290                    computeBboxAndCenterScale(bounds);
291                }
292                layer.onPostDownloadFromServer();
293            }
294        }
295    }
296
297    protected class DownloadTask extends AbstractInternalTask {
298        protected final OsmServerReader reader;
299
300        /**
301         * Constructs a new {@code DownloadTask}.
302         * @param newLayer if {@code true}, force download to a new layer
303         * @param reader OSM data reader
304         * @param progressMonitor progress monitor
305         */
306        public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
307            this(newLayer, reader, progressMonitor, true);
308        }
309
310        /**
311         * Constructs a new {@code DownloadTask}.
312         * @param newLayer if {@code true}, force download to a new layer
313         * @param reader OSM data reader
314         * @param progressMonitor progress monitor
315         * @param zoomAfterDownload If true, the map view will zoom to download area after download
316         * @since 8942
317         */
318        public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) {
319            super(newLayer, tr("Downloading data"), progressMonitor, false, zoomAfterDownload);
320            this.reader = reader;
321        }
322
323        protected DataSet parseDataSet() throws OsmTransferException {
324            return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
325        }
326
327        @Override
328        public void realRun() throws IOException, SAXException, OsmTransferException {
329            try {
330                if (isCanceled())
331                    return;
332                dataSet = parseDataSet();
333            } catch (Exception e) {
334                if (isCanceled()) {
335                    Main.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
336                    return;
337                }
338                if (e instanceof OsmTransferCanceledException) {
339                    setCanceled(true);
340                    return;
341                } else if (e instanceof OsmTransferException) {
342                    rememberException(e);
343                } else {
344                    rememberException(new OsmTransferException(e));
345                }
346                DownloadOsmTask.this.setFailed(true);
347            }
348        }
349
350        @Override
351        protected void finish() {
352            if (isFailed() || isCanceled())
353                return;
354            if (dataSet == null)
355                return; // user canceled download or error occurred
356            if (dataSet.allPrimitives().isEmpty()) {
357                if (warnAboutEmptyArea) {
358                    rememberErrorMessage(tr("No data found in this area."));
359                }
360                // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work
361                dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds :
362                    new Bounds(new LatLon(0, 0)), "OpenStreetMap server"));
363            }
364
365            rememberDownloadedData(dataSet);
366            loadData(newLayerName, currentBounds);
367        }
368
369        @Override
370        protected void cancel() {
371            setCanceled(true);
372            if (reader != null) {
373                reader.cancel();
374            }
375        }
376    }
377
378    @Override
379    public String getConfirmationMessage(URL url) {
380        if (url != null) {
381            String urlString = url.toExternalForm();
382            if (urlString.matches(PATTERN_OSM_API_URL)) {
383                // TODO: proper i18n after stabilization
384                Collection<String> items = new ArrayList<>();
385                items.add(tr("OSM Server URL:") + ' ' + url.getHost());
386                items.add(tr("Command")+": "+url.getPath());
387                if (url.getQuery() != null) {
388                    items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")));
389                }
390                return Utils.joinAsHtmlUnorderedList(items);
391            }
392            // TODO: other APIs
393        }
394        return null;
395    }
396}