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