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.net.HttpURLConnection; 012import java.net.MalformedURLException; 013import java.net.URL; 014import java.net.URLConnection; 015import java.nio.charset.StandardCharsets; 016import java.text.DecimalFormat; 017import java.text.DecimalFormatSymbols; 018import java.text.NumberFormat; 019import java.util.HashMap; 020import java.util.Locale; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.data.coor.EastNorth; 028import org.openstreetmap.josm.data.coor.LatLon; 029import org.openstreetmap.josm.data.imagery.GeorefImage.State; 030import org.openstreetmap.josm.data.imagery.ImageryInfo; 031import org.openstreetmap.josm.gui.MapView; 032import org.openstreetmap.josm.gui.layer.WMSLayer; 033import org.openstreetmap.josm.io.OsmTransferException; 034import org.openstreetmap.josm.io.ProgressInputStream; 035import org.openstreetmap.josm.tools.ImageProvider; 036import org.openstreetmap.josm.tools.Utils; 037 038public class WMSGrabber extends Grabber { 039 040 protected String baseURL; 041 private ImageryInfo info; 042 private Map<String, String> props = new HashMap<>(); 043 044 public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) { 045 super(mv, layer, localOnly); 046 this.info = layer.getInfo(); 047 this.baseURL = info.getUrl(); 048 if(layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) { 049 props.put("Cookie", layer.getInfo().getCookies()); 050 } 051 Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}"); 052 StringBuffer output = new StringBuffer(); 053 Matcher matcher = pattern.matcher(this.baseURL); 054 while (matcher.find()) { 055 props.put(matcher.group(1),matcher.group(2)); 056 matcher.appendReplacement(output, ""); 057 } 058 matcher.appendTail(output); 059 this.baseURL = output.toString(); 060 } 061 062 @Override 063 void fetch(WMSRequest request, int attempt) throws Exception{ 064 URL url = null; 065 try { 066 url = getURL( 067 b.minEast, b.minNorth, 068 b.maxEast, b.maxNorth, 069 width(), height()); 070 request.finish(State.IMAGE, grab(request, url, attempt)); 071 072 } catch(Exception e) { 073 Main.error(e); 074 throw new Exception(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e); 075 } 076 } 077 078 public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", 079 new DecimalFormatSymbols(Locale.US)); 080 081 protected URL getURL(double w, double s,double e,double n, 082 int wi, int ht) throws MalformedURLException { 083 String myProj = Main.getProjection().toCode(); 084 if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) { 085 LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s)); 086 LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n)); 087 myProj = "EPSG:4326"; 088 s = sw.lat(); 089 w = sw.lon(); 090 n = ne.lat(); 091 e = ne.lon(); 092 } 093 if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) { 094 myProj = "CRS:84"; 095 } 096 097 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. 098 // 099 // Background: 100 // 101 // bbox=x_min,y_min,x_max,y_max 102 // 103 // SRS=... is WMS 1.1.1 104 // CRS=... is WMS 1.3.0 105 // 106 // The difference: 107 // For SRS x is east-west and y is north-south 108 // For CRS x and y are as specified by the EPSG 109 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. 110 // For most other EPSG code there seems to be no difference. 111 // [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 112 boolean switchLatLon = false; 113 if (baseURL.toLowerCase().contains("crs=epsg:4326")) { 114 switchLatLon = true; 115 } else if (baseURL.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProj)) { 116 switchLatLon = true; 117 } 118 String bbox; 119 if (switchLatLon) { 120 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e)); 121 } else { 122 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n)); 123 } 124 return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj) 125 .replaceAll("\\{bbox\\}", bbox) 126 .replaceAll("\\{w\\}", latLonFormat.format(w)) 127 .replaceAll("\\{s\\}", latLonFormat.format(s)) 128 .replaceAll("\\{e\\}", latLonFormat.format(e)) 129 .replaceAll("\\{n\\}", latLonFormat.format(n)) 130 .replaceAll("\\{width\\}", String.valueOf(wi)) 131 .replaceAll("\\{height\\}", String.valueOf(ht)) 132 .replace(" ", "%20")); 133 } 134 135 @Override 136 public boolean loadFromCache(WMSRequest request) { 137 BufferedImage cached = layer.cache.getExactMatch( 138 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 139 140 if (cached != null) { 141 request.finish(State.IMAGE, cached); 142 return true; 143 } else if (request.isAllowPartialCacheMatch()) { 144 BufferedImage partialMatch = layer.cache.getPartialMatch( 145 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 146 if (partialMatch != null) { 147 request.finish(State.PARTLY_IN_CACHE, partialMatch); 148 return true; 149 } 150 } 151 152 if((!request.isReal() && !layer.hasAutoDownload())){ 153 request.finish(State.NOT_IN_CACHE, null); 154 return true; 155 } 156 157 return false; 158 } 159 160 protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException, OsmTransferException { 161 Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url); 162 163 HttpURLConnection conn = Utils.openHttpConnection(url); 164 for(Entry<String, String> e : props.entrySet()) { 165 conn.setRequestProperty(e.getKey(), e.getValue()); 166 } 167 conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000); 168 conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000); 169 170 String contentType = conn.getHeaderField("Content-Type"); 171 if( conn.getResponseCode() != 200 172 || contentType != null && !contentType.startsWith("image") ) 173 throw new IOException(readException(conn)); 174 175 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 176 try (InputStream is = new ProgressInputStream(conn, null)) { 177 Utils.copyStream(is, baos); 178 } 179 180 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 181 BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get())); 182 bais.reset(); 183 layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth); 184 return img; 185 } 186 187 protected String readException(URLConnection conn) throws IOException { 188 StringBuilder exception = new StringBuilder(); 189 InputStream in = conn.getInputStream(); 190 try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { 191 String line = null; 192 while( (line = br.readLine()) != null) { 193 // filter non-ASCII characters and control characters 194 exception.append(line.replaceAll("[^\\p{Print}]", "")); 195 exception.append('\n'); 196 } 197 return exception.toString(); 198 } 199 } 200}