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