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