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