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