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