001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.imagery; 003 004import java.awt.image.BufferedImage; 005import java.io.BufferedReader; 006import java.io.ByteArrayInputStream; 007import java.io.ByteArrayOutputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.InputStreamReader; 011import java.io.StringReader; 012import java.net.HttpURLConnection; 013import java.net.MalformedURLException; 014import java.net.URL; 015import java.net.URLConnection; 016import java.nio.charset.StandardCharsets; 017import java.text.DecimalFormat; 018import java.text.DecimalFormatSymbols; 019import java.text.NumberFormat; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import javax.xml.parsers.DocumentBuilder; 030import javax.xml.parsers.DocumentBuilderFactory; 031import javax.xml.parsers.ParserConfigurationException; 032 033import org.openstreetmap.josm.Main; 034import org.openstreetmap.josm.data.ProjectionBounds; 035import org.openstreetmap.josm.data.coor.EastNorth; 036import org.openstreetmap.josm.data.coor.LatLon; 037import org.openstreetmap.josm.data.imagery.GeorefImage.State; 038import org.openstreetmap.josm.data.imagery.ImageryInfo; 039import org.openstreetmap.josm.gui.MapView; 040import org.openstreetmap.josm.gui.layer.WMSLayer; 041import org.openstreetmap.josm.io.OsmTransferException; 042import org.openstreetmap.josm.io.ProgressInputStream; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.Utils; 045import org.w3c.dom.Document; 046import org.w3c.dom.NodeList; 047import org.xml.sax.InputSource; 048import org.xml.sax.SAXException; 049 050/** 051 * WMS grabber, fetching tiles from WMS server. 052 * @since 3715 053 */ 054public class WMSGrabber implements Runnable { 055 056 protected final MapView mv; 057 protected final WMSLayer layer; 058 private final boolean localOnly; 059 060 protected ProjectionBounds b; 061 protected volatile boolean canceled; 062 063 protected String baseURL; 064 private ImageryInfo info; 065 private Map<String, String> props = new HashMap<>(); 066 067 /** 068 * Constructs a new {@code WMSGrabber}. 069 * @param mv Map view 070 * @param layer WMS layer 071 */ 072 public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) { 073 this.mv = mv; 074 this.layer = layer; 075 this.localOnly = localOnly; 076 this.info = layer.getInfo(); 077 this.baseURL = info.getUrl(); 078 if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) { 079 props.put("Cookie", layer.getInfo().getCookies()); 080 } 081 Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}"); 082 StringBuffer output = new StringBuffer(); 083 Matcher matcher = pattern.matcher(this.baseURL); 084 while (matcher.find()) { 085 props.put(matcher.group(1),matcher.group(2)); 086 matcher.appendReplacement(output, ""); 087 } 088 matcher.appendTail(output); 089 this.baseURL = output.toString(); 090 } 091 092 int width() { 093 return layer.getBaseImageWidth(); 094 } 095 096 int height() { 097 return layer.getBaseImageHeight(); 098 } 099 100 @Override 101 public void run() { 102 while (true) { 103 if (canceled) 104 return; 105 WMSRequest request = layer.getRequest(localOnly); 106 if (request == null) 107 return; 108 this.b = layer.getBounds(request); 109 if (request.isPrecacheOnly()) { 110 if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) { 111 attempt(request); 112 } else if (Main.isDebugEnabled()) { 113 Main.debug("Ignoring "+request+" (precache only + exact match)"); 114 } 115 } else if (!loadFromCache(request)){ 116 attempt(request); 117 } else if (Main.isDebugEnabled()) { 118 Main.debug("Ignoring "+request+" (loaded from cache)"); 119 } 120 layer.finishRequest(request); 121 } 122 } 123 124 protected void attempt(WMSRequest request){ // try to fetch the image 125 int maxTries = 5; // n tries for every image 126 for (int i = 1; i <= maxTries; i++) { 127 if (canceled) 128 return; 129 try { 130 if (!request.isPrecacheOnly() && !layer.requestIsVisible(request)) 131 return; 132 fetch(request, i); 133 break; // break out of the retry loop 134 } catch (IOException e) { 135 try { // sleep some time and then ask the server again 136 Thread.sleep(random(1000, 2000)); 137 } catch (InterruptedException e1) { 138 Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request"); 139 } 140 if (i == maxTries) { 141 Main.error(e); 142 request.finish(State.FAILED, null, null); 143 } 144 } catch (WMSException e) { 145 // Fail fast in case of WMS Service exception: useless to retry: 146 // either the URL is wrong or the server suffers huge problems 147 Main.error("WMS service exception while requesting "+e.getUrl()+":\n"+e.getMessage().trim()); 148 request.finish(State.FAILED, null, e); 149 break; // break out of the retry loop 150 } 151 } 152 } 153 154 public static int random(int min, int max) { 155 return (int)(Math.random() * ((max+1)-min) ) + min; 156 } 157 158 public final void cancel() { 159 canceled = true; 160 } 161 162 private void fetch(WMSRequest request, int attempt) throws IOException, WMSException { 163 URL url = null; 164 try { 165 url = getURL( 166 b.minEast, b.minNorth, 167 b.maxEast, b.maxNorth, 168 width(), height()); 169 request.finish(State.IMAGE, grab(request, url, attempt), null); 170 171 } catch (IOException | OsmTransferException e) { 172 Main.error(e); 173 throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e); 174 } 175 } 176 177 public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US)); 178 179 protected URL getURL(double w, double s,double e,double n, 180 int wi, int ht) throws MalformedURLException { 181 String myProj = Main.getProjection().toCode(); 182 if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) { 183 LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s)); 184 LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n)); 185 myProj = "EPSG:4326"; 186 s = sw.lat(); 187 w = sw.lon(); 188 n = ne.lat(); 189 e = ne.lon(); 190 } 191 if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) { 192 myProj = "CRS:84"; 193 } 194 195 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. 196 // 197 // Background: 198 // 199 // bbox=x_min,y_min,x_max,y_max 200 // 201 // SRS=... is WMS 1.1.1 202 // CRS=... is WMS 1.3.0 203 // 204 // The difference: 205 // For SRS x is east-west and y is north-south 206 // For CRS x and y are as specified by the EPSG 207 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. 208 // For most other EPSG code there seems to be no difference. 209 // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326 210 boolean switchLatLon = false; 211 if (baseURL.toLowerCase().contains("crs=epsg:4326")) { 212 switchLatLon = true; 213 } else if (baseURL.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProj)) { 214 switchLatLon = true; 215 } 216 String bbox; 217 if (switchLatLon) { 218 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e)); 219 } else { 220 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n)); 221 } 222 return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj) 223 .replaceAll("\\{bbox\\}", bbox) 224 .replaceAll("\\{w\\}", latLonFormat.format(w)) 225 .replaceAll("\\{s\\}", latLonFormat.format(s)) 226 .replaceAll("\\{e\\}", latLonFormat.format(e)) 227 .replaceAll("\\{n\\}", latLonFormat.format(n)) 228 .replaceAll("\\{width\\}", String.valueOf(wi)) 229 .replaceAll("\\{height\\}", String.valueOf(ht)) 230 .replace(" ", "%20")); 231 } 232 233 public boolean loadFromCache(WMSRequest request) { 234 BufferedImage cached = layer.cache.getExactMatch( 235 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 236 237 if (cached != null) { 238 request.finish(State.IMAGE, cached, null); 239 return true; 240 } else if (request.isAllowPartialCacheMatch()) { 241 BufferedImage partialMatch = layer.cache.getPartialMatch( 242 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 243 if (partialMatch != null) { 244 request.finish(State.PARTLY_IN_CACHE, partialMatch, null); 245 return true; 246 } 247 } 248 249 if ((!request.isReal() && !layer.hasAutoDownload())){ 250 request.finish(State.NOT_IN_CACHE, null, null); 251 return true; 252 } 253 254 return false; 255 } 256 257 protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException { 258 Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url); 259 260 HttpURLConnection conn = Utils.openHttpConnection(url); 261 conn.setUseCaches(true); 262 for (Entry<String, String> e : props.entrySet()) { 263 conn.setRequestProperty(e.getKey(), e.getValue()); 264 } 265 conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000); 266 conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000); 267 268 String contentType = conn.getHeaderField("Content-Type"); 269 if (conn.getResponseCode() != 200 270 || contentType != null && !contentType.startsWith("image") ) { 271 String xml = readException(conn); 272 try { 273 DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 274 InputSource is = new InputSource(new StringReader(xml)); 275 Document doc = db.parse(is); 276 NodeList nodes = doc.getElementsByTagName("ServiceException"); 277 List<String> exceptions = new ArrayList<>(nodes.getLength()); 278 for (int i = 0; i < nodes.getLength(); i++) { 279 exceptions.add(nodes.item(i).getTextContent()); 280 } 281 throw new WMSException(request, url, exceptions); 282 } catch (SAXException | ParserConfigurationException ex) { 283 throw new IOException(xml, ex); 284 } 285 } 286 287 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 288 try (InputStream is = new ProgressInputStream(conn, null)) { 289 Utils.copyStream(is, baos); 290 } 291 292 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 293 BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get())); 294 bais.reset(); 295 layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 296 return img; 297 } 298 299 protected String readException(URLConnection conn) throws IOException { 300 StringBuilder exception = new StringBuilder(); 301 InputStream in = conn.getInputStream(); 302 try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { 303 String line = null; 304 while( (line = br.readLine()) != null) { 305 // filter non-ASCII characters and control characters 306 exception.append(line.replaceAll("[^\\p{Print}]", "")); 307 exception.append('\n'); 308 } 309 return exception.toString(); 310 } 311 } 312}