001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.downloadtasks;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.io.IOException;
007    import java.util.ArrayList;
008    import java.util.Collection;
009    import java.util.HashSet;
010    import java.util.Iterator;
011    import java.util.List;
012    import java.util.Set;
013    import java.util.concurrent.Future;
014    import java.util.regex.Matcher;
015    import java.util.regex.Pattern;
016    
017    import javax.swing.JOptionPane;
018    import org.openstreetmap.josm.Main;
019    import org.openstreetmap.josm.data.Bounds;
020    import org.openstreetmap.josm.data.coor.LatLon;
021    import org.openstreetmap.josm.data.imagery.ImageryInfo;
022    import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
023    import org.openstreetmap.josm.data.imagery.Shape;
024    import org.openstreetmap.josm.data.osm.DataSet;
025    import org.openstreetmap.josm.data.osm.DataSource;
026    import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
027    import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
028    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
029    import org.openstreetmap.josm.gui.layer.Layer;
030    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
031    import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
032    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
033    import org.openstreetmap.josm.io.BoundingBoxDownloader;
034    import org.openstreetmap.josm.io.OsmServerLocationReader;
035    import org.openstreetmap.josm.io.OsmServerReader;
036    import org.openstreetmap.josm.io.OsmTransferCanceledException;
037    import org.openstreetmap.josm.io.OsmTransferException;
038    import org.openstreetmap.josm.tools.Utils;
039    import org.xml.sax.SAXException;
040    
041    /**
042     * Open the download dialog and download the data.
043     * Run in the worker thread.
044     */
045    public class DownloadOsmTask extends AbstractDownloadTask {
046        protected Bounds currentBounds;
047        protected DataSet downloadedData;
048        protected DownloadTask downloadTask;
049        
050        protected OsmDataLayer targetLayer;
051        
052        protected String newLayerName = null;
053    
054        protected void rememberDownloadedData(DataSet ds) {
055            this.downloadedData = ds;
056        }
057    
058        /**
059         * Replies the {@link DataSet} containing the downloaded OSM data.
060         * @return The {@link DataSet} containing the downloaded OSM data.
061         */
062        public DataSet getDownloadedData() {
063            return downloadedData;
064        }
065    
066        @Override
067        public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
068            return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor);
069        }
070    
071        /**
072         * Asynchronously launches the download task for a given bounding box.
073         *
074         * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor.
075         * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to
076         * be discarded.
077         *
078         * You can wait for the asynchronous download task to finish by synchronizing on the returned
079         * {@link Future}, but make sure not to freeze up JOSM. Example:
080         * <pre>
081         *    Future<?> future = task.download(...);
082         *    // DON'T run this on the Swing EDT or JOSM will freeze
083         *    future.get(); // waits for the dowload task to complete
084         * </pre>
085         *
086         * The following example uses a pattern which is better suited if a task is launched from
087         * the Swing EDT:
088         * <pre>
089         *    final Future<?> future = task.download(...);
090         *    Runnable runAfterTask = new Runnable() {
091         *       public void run() {
092         *           // this is not strictly necessary because of the type of executor service
093         *           // Main.worker is initialized with, but it doesn't harm either
094         *           //
095         *           future.get(); // wait for the download task to complete
096         *           doSomethingAfterTheTaskCompleted();
097         *       }
098         *    }
099         *    Main.worker.submit(runAfterTask);
100         * </pre>
101         * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm})
102         * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task
103         *                 selects one of the existing layers as download layer, preferably the active layer.
104         * @param downloadArea the area to download
105         * @param progressMonitor the progressMonitor
106         * @return the future representing the asynchronous task
107         */
108        public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
109            return download(new DownloadTask(newLayer, reader, progressMonitor), downloadArea);
110        }
111    
112        protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) {
113            this.downloadTask = downloadTask;
114            this.currentBounds = new Bounds(downloadArea);
115            // We need submit instead of execute so we can wait for it to finish and get the error
116            // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
117            return Main.worker.submit(downloadTask);
118        }
119    
120        /**
121         * Loads a given URL from the OSM Server
122         * @param new_layer True if the data should be saved to a new layer
123         * @param url The URL as String
124         */
125        public Future<?> loadUrl(boolean new_layer, String url, ProgressMonitor progressMonitor) {
126            downloadTask = new DownloadTask(new_layer,
127                    new OsmServerLocationReader(url),
128                    progressMonitor);
129            currentBounds = null;
130            // Extract .osm filename from URL to set the new layer name
131            extractOsmFilename("http://.*/(.*\\.osm)", url);
132            return Main.worker.submit(downloadTask);
133        }
134        
135        protected final void extractOsmFilename(String pattern, String url) {
136            Matcher matcher = Pattern.compile(pattern).matcher(url);
137            newLayerName = matcher.matches() ? matcher.group(1) : null;
138        }
139        
140        /* (non-Javadoc)
141         * @see org.openstreetmap.josm.actions.downloadtasks.DownloadTask#acceptsUrl(java.lang.String)
142         */
143        @Override
144        public boolean acceptsUrl(String url) {
145            return url != null && (
146                    url.matches("http://.*/api/0.6/(map|nodes?|ways?|relations?|\\*).*")// OSM API 0.6 and XAPI
147                 || url.matches("http://.*/interpreter\\?data=.*")                      // Overpass API
148                 || url.matches("http://.*/xapi\\?.*\\[@meta\\].*")                     // Overpass API XAPI compatibility layer
149                 || url.matches("http://.*/.*\\.osm")                                   // Remote .osm files
150                    );
151        }
152    
153        public void cancel() {
154            if (downloadTask != null) {
155                downloadTask.cancel();
156            }
157        }
158    
159        protected class DownloadTask extends PleaseWaitRunnable {
160            protected OsmServerReader reader;
161            protected DataSet dataSet;
162            protected boolean newLayer;
163    
164            public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
165                super(tr("Downloading data"), progressMonitor, false);
166                this.reader = reader;
167                this.newLayer = newLayer;
168            }
169            
170            protected DataSet parseDataSet() throws OsmTransferException {
171                return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
172            }
173    
174            @Override public void realRun() throws IOException, SAXException, OsmTransferException {
175                try {
176                    if (isCanceled())
177                        return;
178                    dataSet = parseDataSet();
179                } catch(Exception e) {
180                    if (isCanceled()) {
181                        System.out.println(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
182                        return;
183                    }
184                    if (e instanceof OsmTransferCanceledException) {
185                        setCanceled(true);
186                        return;
187                    } else if (e instanceof OsmTransferException) {
188                        rememberException(e);
189                    } else {
190                        rememberException(new OsmTransferException(e));
191                    }
192                    DownloadOsmTask.this.setFailed(true);
193                }
194            }
195    
196            protected OsmDataLayer getEditLayer() {
197                if (!Main.isDisplayingMapView()) return null;
198                return Main.map.mapView.getEditLayer();
199            }
200    
201            protected int getNumDataLayers() {
202                int count = 0;
203                if (!Main.isDisplayingMapView()) return 0;
204                Collection<Layer> layers = Main.map.mapView.getAllLayers();
205                for (Layer layer : layers) {
206                    if (layer instanceof OsmDataLayer) {
207                        count++;
208                    }
209                }
210                return count;
211            }
212    
213            protected OsmDataLayer getFirstDataLayer() {
214                if (!Main.isDisplayingMapView()) return null;
215                Collection<Layer> layers = Main.map.mapView.getAllLayersAsList();
216                for (Layer layer : layers) {
217                    if (layer instanceof OsmDataLayer)
218                        return (OsmDataLayer) layer;
219                }
220                return null;
221            }
222            
223            protected OsmDataLayer createNewLayer(String layerName) {
224                if (layerName == null || layerName.isEmpty()) {
225                    layerName = OsmDataLayer.createNewName();
226                }
227                return new OsmDataLayer(dataSet, layerName, null);
228            }
229            
230            protected OsmDataLayer createNewLayer() {
231                return createNewLayer(null);
232            }
233    
234            @Override protected void finish() {
235                if (isFailed() || isCanceled())
236                    return;
237                if (dataSet == null)
238                    return; // user canceled download or error occurred
239                if (dataSet.allPrimitives().isEmpty()) {
240                    rememberErrorMessage(tr("No data found in this area."));
241                    // need to synthesize a download bounds lest the visual indication of downloaded
242                    // area doesn't work
243                    dataSet.dataSources.add(new DataSource(currentBounds != null ? currentBounds : new Bounds(new LatLon(0, 0)), "OpenStreetMap server"));
244                }
245    
246                rememberDownloadedData(dataSet);
247                int numDataLayers = getNumDataLayers();
248                if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
249                    // the user explicitly wants a new layer, we don't have any layer at all
250                    // or it is not clear which layer to merge to
251                    //
252                    targetLayer = createNewLayer(newLayerName);
253                    final boolean isDisplayingMapView = Main.isDisplayingMapView();
254    
255                    Main.main.addLayer(targetLayer);
256    
257                    // If the mapView is not there yet, we cannot calculate the bounds (see constructor of MapView).
258                    // Otherwise jump to the current download.
259                    if (isDisplayingMapView) {
260                        computeBboxAndCenterScale();
261                    }
262                } else {
263                    targetLayer = getEditLayer();
264                    if (targetLayer == null) {
265                        targetLayer = getFirstDataLayer();
266                    }
267                    targetLayer.mergeFrom(dataSet);
268                    computeBboxAndCenterScale();
269                    targetLayer.onPostDownloadFromServer();
270                }
271    
272                suggestImageryLayers();
273            }
274            
275            protected void computeBboxAndCenterScale() {
276                BoundingXYVisitor v = new BoundingXYVisitor();
277                if (currentBounds != null) {
278                    v.visit(currentBounds);
279                } else {
280                    v.computeBoundingBox(dataSet.getNodes());
281                }
282                Main.map.mapView.recalculateCenterScale(v);
283            }
284    
285            @Override protected void cancel() {
286                setCanceled(true);
287                if (reader != null) {
288                    reader.cancel();
289                }
290            }
291    
292            protected void suggestImageryLayers() {
293                if (currentBounds != null) {
294                    final LatLon center = currentBounds.getCenter();
295                    final Set<ImageryInfo> layers = new HashSet<ImageryInfo>();
296        
297                    for (ImageryInfo i : ImageryLayerInfo.instance.getDefaultLayers()) {
298                        if (i.getBounds() != null && i.getBounds().contains(center)) {
299                            layers.add(i);
300                        }
301                    }
302                    // Do not suggest layers already in use
303                    layers.removeAll(ImageryLayerInfo.instance.getLayers());
304                    // For layers containing complex shapes, check that center is in one of its shapes (fix #7910)
305                    for (Iterator<ImageryInfo> iti = layers.iterator(); iti.hasNext(); ) {
306                        List<Shape> shapes = iti.next().getBounds().getShapes();
307                        if (shapes != null && !shapes.isEmpty()) {
308                            boolean found = false;
309                            for (Iterator<Shape> its = shapes.iterator(); its.hasNext() && !found; ) {
310                                found = its.next().contains(center);
311                            }
312                            if (!found) {
313                                iti.remove();
314                            }
315                        }
316                    }
317        
318                    if (layers.isEmpty()) {
319                        return;
320                    }
321        
322                    final List<String> layerNames = new ArrayList<String>();
323                    for (ImageryInfo i : layers) {
324                        layerNames.add(i.getName());
325                    }
326        
327                    if (!ConditionalOptionPaneUtil.showConfirmationDialog(
328                            "download.suggest-imagery-layer",
329                            Main.parent,
330                            tr("<html>For the downloaded area, the following additional imagery layers are available: {0}" +
331                                    "Do you want to add those layers to the <em>Imagery</em> menu?" +
332                                    "<br>(If needed, you can remove those entries in the <em>Preferences</em>.)",
333                                    Utils.joinAsHtmlUnorderedList(layerNames)),
334                            tr("Add imagery layers?"),
335                            JOptionPane.YES_NO_OPTION,
336                            JOptionPane.QUESTION_MESSAGE,
337                            JOptionPane.YES_OPTION)) {
338                        return;
339                    }
340        
341                    ImageryLayerInfo.addLayers(layers);
342                }
343            }
344        }
345    }