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