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