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