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 result.mergeFrom(downloadRawGps(new Bounds(lat1, -180.0, lat2, lon2), progressMonitor)); 097 return result; 098 } else { 099 // Simple request 100 return downloadRawGps(new Bounds(lat1, lon1, lat2, lon2), progressMonitor); 101 } 102 } catch (IllegalArgumentException e) { 103 // caused by HttpUrlConnection in case of illegal stuff in the response 104 if (cancel) 105 return null; 106 throw new OsmTransferException("Illegal characters within the HTTP-header response.", e); 107 } catch (IOException e) { 108 if (cancel) 109 return null; 110 throw new OsmTransferException(e); 111 } catch (SAXException e) { 112 throw new OsmTransferException(e); 113 } catch (OsmTransferException e) { 114 throw e; 115 } catch (RuntimeException e) { 116 if (cancel) 117 return null; 118 throw e; 119 } finally { 120 progressMonitor.finishTask(); 121 } 122 } 123 124 /** 125 * Returns the name of the download task to be displayed in the {@link ProgressMonitor}. 126 * @return task name 127 */ 128 protected String getTaskName() { 129 return tr("Contacting OSM Server..."); 130 } 131 132 /** 133 * Builds the request part for the bounding box. 134 * @param lon1 left 135 * @param lat1 bottom 136 * @param lon2 right 137 * @param lat2 top 138 * @return "map?bbox=left,bottom,right,top" 139 */ 140 protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) { 141 return "map?bbox=" + lon1 + ',' + lat1 + ',' + lon2 + ',' + lat2; 142 } 143 144 /** 145 * Parse the given input source and return the dataset. 146 * @param source input stream 147 * @param progressMonitor progress monitor 148 * @return dataset 149 * @throws IllegalDataException if an error was found while parsing the OSM data 150 * 151 * @see OsmReader#parseDataSet(InputStream, ProgressMonitor) 152 */ 153 protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 154 return OsmReader.parseDataSet(source, progressMonitor); 155 } 156 157 @Override 158 public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException { 159 progressMonitor.beginTask(getTaskName(), 10); 160 try { 161 DataSet ds = null; 162 progressMonitor.indeterminateSubTask(null); 163 if (crosses180th) { 164 // API 0.6 does not support requests crossing the 180th meridian, so make two requests 165 DataSet ds2 = null; 166 167 try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, 180.0, lat2), 168 progressMonitor.createSubTaskMonitor(9, false))) { 169 if (in == null) 170 return null; 171 ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false)); 172 } 173 174 try (InputStream in = getInputStream(getRequestForBbox(-180.0, lat1, lon2, lat2), 175 progressMonitor.createSubTaskMonitor(9, false))) { 176 if (in == null) 177 return null; 178 ds2 = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false)); 179 } 180 if (ds2 == null) 181 return null; 182 ds.mergeFrom(ds2); 183 184 } else { 185 // Simple request 186 try (InputStream in = getInputStream(getRequestForBbox(lon1, lat1, lon2, lat2), 187 progressMonitor.createSubTaskMonitor(9, false))) { 188 if (in == null) 189 return null; 190 ds = parseDataSet(in, progressMonitor.createSubTaskMonitor(1, false)); 191 } 192 } 193 return ds; 194 } catch (OsmTransferException e) { 195 throw e; 196 } catch (IllegalDataException | IOException e) { 197 throw new OsmTransferException(e); 198 } finally { 199 progressMonitor.finishTask(); 200 activeConnection = null; 201 } 202 } 203 204 @Override 205 public List<Note> parseNotes(int noteLimit, int daysClosed, ProgressMonitor progressMonitor) 206 throws OsmTransferException, MoreNotesException { 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 <= 10000, "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}