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