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                if (result != null)
097                    result.mergeFrom(downloadRawGps(new Bounds(lat1, -180.0, lat2, lon2), progressMonitor));
098                return result;
099            } else {
100                // Simple request
101                return downloadRawGps(new Bounds(lat1, lon1, lat2, lon2), progressMonitor);
102            }
103        } catch (IllegalArgumentException e) {
104            // caused by HttpUrlConnection in case of illegal stuff in the response
105            if (cancel)
106                return null;
107            throw new OsmTransferException("Illegal characters within the HTTP-header response.", e);
108        } catch (IOException e) {
109            if (cancel)
110                return null;
111            throw new OsmTransferException(e);
112        } catch (SAXException e) {
113            throw new OsmTransferException(e);
114        } catch (OsmTransferException e) {
115            throw e;
116        } catch (RuntimeException e) {
117            if (cancel)
118                return null;
119            throw e;
120        } finally {
121            progressMonitor.finishTask();
122        }
123    }
124
125    /**
126     * Returns the name of the download task to be displayed in the {@link ProgressMonitor}.
127     * @return task name
128     */
129    protected String getTaskName() {
130        return tr("Contacting OSM Server...");
131    }
132
133    /**
134     * Builds the request part for the bounding box.
135     * @param lon1 left
136     * @param lat1 bottom
137     * @param lon2 right
138     * @param lat2 top
139     * @return "map?bbox=left,bottom,right,top"
140     */
141    protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
142        return "map?bbox=" + lon1 + ',' + lat1 + ',' + lon2 + ',' + lat2;
143    }
144
145    /**
146     * Parse the given input source and return the dataset.
147     * @param source input stream
148     * @param progressMonitor progress monitor
149     * @return dataset
150     * @throws IllegalDataException if an error was found while parsing the OSM data
151     *
152     * @see OsmReader#parseDataSet(InputStream, ProgressMonitor)
153     */
154    protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
155        return OsmReader.parseDataSet(source, progressMonitor);
156    }
157
158    @Override
159    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
160        progressMonitor.beginTask(getTaskName(), 10);
161        try {
162            DataSet ds = null;
163            progressMonitor.indeterminateSubTask(null);
164            if (crosses180th) {
165                // API 0.6 does not support requests crossing the 180th meridian, so make two requests
166                DataSet ds2 = null;
167
168                try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, 180.0, lat2),
169                        progressMonitor.createSubTaskMonitor(9, false))) {
170                    if (in == null)
171                        return null;
172                    ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
173                }
174
175                try (InputStream in = getInputStream(getRequestForBbox(-180.0, lat1, lon2, lat2),
176                        progressMonitor.createSubTaskMonitor(9, false))) {
177                    if (in == null)
178                        return null;
179                    ds2 = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
180                }
181                if (ds2 == null)
182                    return null;
183                ds.mergeFrom(ds2);
184
185            } else {
186                // Simple request
187                try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, lon2, lat2),
188                        progressMonitor.createSubTaskMonitor(9, false))) {
189                    if (in == null)
190                        return null;
191                    ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false));
192                }
193            }
194            return ds;
195        } catch (OsmTransferException e) {
196            throw e;
197        } catch (IllegalDataException | IOException e) {
198            throw new OsmTransferException(e);
199        } finally {
200            progressMonitor.finishTask();
201            activeConnection = null;
202        }
203    }
204
205    @Override
206    public List<Note> parseNotes(int noteLimit, int daysClosed, ProgressMonitor progressMonitor) throws OsmTransferException {
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 <= 10_000, "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}