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