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