001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.net.SocketException;
009import java.util.List;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.Bounds;
013import org.openstreetmap.josm.data.DataSource;
014import org.openstreetmap.josm.data.gpx.GpxData;
015import org.openstreetmap.josm.data.notes.Note;
016import org.openstreetmap.josm.data.osm.DataSet;
017import org.openstreetmap.josm.gui.progress.ProgressMonitor;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019import org.xml.sax.SAXException;
020
021/**
022 * Read content from OSM server for a given bounding box
023 * @since 627
024 */
025public class BoundingBoxDownloader extends OsmServerReader {
026
027    /**
028     * The boundings of the desired map data.
029     */
030    protected final double lat1;
031    protected final double lon1;
032    protected final double lat2;
033    protected final double lon2;
034    protected final boolean crosses180th;
035
036    /**
037     * Constructs a new {@code BoundingBoxDownloader}.
038     * @param downloadArea The area to download
039     */
040    public BoundingBoxDownloader(Bounds downloadArea) {
041        CheckParameterUtil.ensureParameterNotNull(downloadArea, "downloadArea");
042        this.lat1 = downloadArea.getMinLat();
043        this.lon1 = downloadArea.getMinLon();
044        this.lat2 = downloadArea.getMaxLat();
045        this.lon2 = downloadArea.getMaxLon();
046        this.crosses180th = downloadArea.crosses180thMeridian();
047    }
048
049    private GpxData downloadRawGps(Bounds b, ProgressMonitor progressMonitor) throws IOException, OsmTransferException, SAXException {
050        boolean done = false;
051        GpxData result = null;
052        String url = "trackpoints?bbox="+b.getMinLon()+','+b.getMinLat()+','+b.getMaxLon()+','+b.getMaxLat()+"&page=";
053        for (int i = 0; !done && !isCanceled(); ++i) {
054            progressMonitor.subTask(tr("Downloading points {0} to {1}...", i * 5000, (i + 1) * 5000));
055            try (InputStream in = getInputStream(url+i, progressMonitor.createSubTaskMonitor(1, true))) {
056                if (in == null) {
057                    break;
058                }
059                progressMonitor.setTicks(0);
060                GpxReader reader = new GpxReader(in);
061                gpxParsedProperly = reader.parse(false);
062                GpxData currentGpx = reader.getGpxData();
063                if (result == null) {
064                    result = currentGpx;
065                } else if (currentGpx.hasTrackPoints()) {
066                    result.mergeFrom(currentGpx);
067                } else {
068                    done = true;
069                }
070            } catch (OsmApiException ex) {
071                throw ex; // this avoids infinite loop in case of API error such as bad request (ex: bbox too large, see #12853)
072            } catch (OsmTransferException | SocketException ex) {
073                if (isCanceled()) {
074                    final OsmTransferCanceledException canceledException = new OsmTransferCanceledException("Operation canceled");
075                    canceledException.initCause(ex);
076                    Main.warn(canceledException);
077                }
078            }
079            activeConnection = null;
080        }
081        if (result != null) {
082            result.fromServer = true;
083            result.dataSources.add(new DataSource(b, "OpenStreetMap server"));
084        }
085        return result;
086    }
087
088    @Override
089    public GpxData parseRawGps(ProgressMonitor progressMonitor) throws OsmTransferException {
090        progressMonitor.beginTask("", 1);
091        try {
092            progressMonitor.indeterminateSubTask(getTaskName());
093            if (crosses180th) {
094                // API 0.6 does not support requests crossing the 180th meridian, so make two requests
095                GpxData result = downloadRawGps(new Bounds(lat1, lon1, lat2, 180.0), progressMonitor);
096                result.mergeFrom(downloadRawGps(new Bounds(lat1, -180.0, lat2, lon2), progressMonitor));
097                return result;
098            } else {
099                // Simple request
100                return downloadRawGps(new Bounds(lat1, lon1, lat2, lon2), progressMonitor);
101            }
102        } catch (IllegalArgumentException e) {
103            // caused by HttpUrlConnection in case of illegal stuff in the response
104            if (cancel)
105                return null;
106            throw new OsmTransferException("Illegal characters within the HTTP-header response.", e);
107        } catch (IOException e) {
108            if (cancel)
109                return null;
110            throw new OsmTransferException(e);
111        } catch (SAXException e) {
112            throw new OsmTransferException(e);
113        } catch (OsmTransferException e) {
114            throw e;
115        } catch (RuntimeException e) {
116            if (cancel)
117                return null;
118            throw e;
119        } finally {
120            progressMonitor.finishTask();
121        }
122    }
123
124    /**
125     * Returns the name of the download task to be displayed in the {@link ProgressMonitor}.
126     * @return task name
127     */
128    protected String getTaskName() {
129        return tr("Contacting OSM Server...");
130    }
131
132    /**
133     * Builds the request part for the bounding box.
134     * @param lon1 left
135     * @param lat1 bottom
136     * @param lon2 right
137     * @param lat2 top
138     * @return "map?bbox=left,bottom,right,top"
139     */
140    protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
141        return "map?bbox=" + lon1 + ',' + lat1 + ',' + lon2 + ',' + lat2;
142    }
143
144    /**
145     * Parse the given input source and return the dataset.
146     * @param source input stream
147     * @param progressMonitor progress monitor
148     * @return dataset
149     * @throws IllegalDataException if an error was found while parsing the OSM data
150     *
151     * @see OsmReader#parseDataSet(InputStream, ProgressMonitor)
152     */
153    protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
154        return OsmReader.parseDataSet(source, progressMonitor);
155    }
156
157    @Override
158    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
159        progressMonitor.beginTask(getTaskName(), 10);
160        try {
161            DataSet ds = null;
162            progressMonitor.indeterminateSubTask(null);
163            if (crosses180th) {
164                // API 0.6 does not support requests crossing the 180th meridian, so make two requests
165                DataSet ds2 = null;
166
167                try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, 180.0, lat2),
168                        progressMonitor.createSubTaskMonitor(9, false))) {
169                    if (in == null)
170                        return null;
171                    ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
172                }
173
174                try (InputStream in = getInputStream(getRequestForBbox(-180.0, lat1, lon2, lat2),
175                        progressMonitor.createSubTaskMonitor(9, false))) {
176                    if (in == null)
177                        return null;
178                    ds2 = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
179                }
180                if (ds2 == null)
181                    return null;
182                ds.mergeFrom(ds2);
183
184            } else {
185                // Simple request
186                try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, lon2, lat2),
187                        progressMonitor.createSubTaskMonitor(9, false))) {
188                    if (in == null)
189                        return null;
190                    ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
191                }
192            }
193            return ds;
194        } catch (OsmTransferException e) {
195            throw e;
196        } catch (IllegalDataException | IOException e) {
197            throw new OsmTransferException(e);
198        } finally {
199            progressMonitor.finishTask();
200            activeConnection = null;
201        }
202    }
203
204    @Override
205    public List<Note> parseNotes(int noteLimit, int daysClosed, ProgressMonitor progressMonitor)
206            throws OsmTransferException, MoreNotesException {
207        progressMonitor.beginTask(tr("Downloading notes"));
208        CheckParameterUtil.ensureThat(noteLimit > 0, "Requested note limit is less than 1.");
209        // see result_limit in https://github.com/openstreetmap/openstreetmap-website/blob/master/app/controllers/notes_controller.rb
210        CheckParameterUtil.ensureThat(noteLimit <= 10000, "Requested note limit is over API hard limit of 10000.");
211        CheckParameterUtil.ensureThat(daysClosed >= -1, "Requested note limit is less than -1.");
212        String url = "notes?limit=" + noteLimit + "&closed=" + daysClosed + "&bbox=" + lon1 + ',' + lat1 + ',' + lon2 + ',' + lat2;
213        try {
214            InputStream is = getInputStream(url, progressMonitor.createSubTaskMonitor(1, false));
215            NoteReader reader = new NoteReader(is);
216            final List<Note> notes = reader.parse();
217            if (notes.size() == noteLimit) {
218                throw new MoreNotesException(notes, noteLimit);
219            }
220            return notes;
221        } catch (IOException | SAXException e) {
222            throw new OsmTransferException(e);
223        } finally {
224            progressMonitor.finishTask();
225        }
226    }
227
228    /**
229     * Indicates that the number of fetched notes equals the specified limit. Thus there might be more notes to download.
230     */
231    public static class MoreNotesException extends RuntimeException {
232        /**
233         * The downloaded notes
234         */
235        public final transient List<Note> notes;
236        /**
237         * The download limit sent to the server.
238         */
239        public final int limit;
240
241        /**
242         * Constructs a {@code MoreNotesException}.
243         * @param notes downloaded notes
244         * @param limit download limit sent to the server
245         */
246        public MoreNotesException(List<Note> notes, int limit) {
247            this.notes = notes;
248            this.limit = limit;
249        }
250    }
251}