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