[3719] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[3715] | 2 | package org.openstreetmap.josm.io.imagery;
|
---|
| 3 |
|
---|
| 4 | import java.awt.image.BufferedImage;
|
---|
| 5 | import java.io.BufferedReader;
|
---|
[4065] | 6 | import java.io.ByteArrayInputStream;
|
---|
| 7 | import java.io.ByteArrayOutputStream;
|
---|
[3715] | 8 | import java.io.IOException;
|
---|
| 9 | import java.io.InputStream;
|
---|
| 10 | import java.io.InputStreamReader;
|
---|
[7425] | 11 | import java.io.StringReader;
|
---|
[3715] | 12 | import java.net.HttpURLConnection;
|
---|
| 13 | import java.net.MalformedURLException;
|
---|
| 14 | import java.net.URL;
|
---|
| 15 | import java.net.URLConnection;
|
---|
[7082] | 16 | import java.nio.charset.StandardCharsets;
|
---|
[3715] | 17 | import java.text.DecimalFormat;
|
---|
| 18 | import java.text.DecimalFormatSymbols;
|
---|
| 19 | import java.text.NumberFormat;
|
---|
[7425] | 20 | import java.util.ArrayList;
|
---|
[4745] | 21 | import java.util.HashMap;
|
---|
[7425] | 22 | import java.util.List;
|
---|
[3715] | 23 | import java.util.Locale;
|
---|
[4228] | 24 | import java.util.Map;
|
---|
[4745] | 25 | import java.util.Map.Entry;
|
---|
[3715] | 26 | import java.util.regex.Matcher;
|
---|
| 27 | import java.util.regex.Pattern;
|
---|
| 28 |
|
---|
[7425] | 29 | import javax.xml.parsers.DocumentBuilder;
|
---|
| 30 | import javax.xml.parsers.DocumentBuilderFactory;
|
---|
| 31 | import javax.xml.parsers.ParserConfigurationException;
|
---|
| 32 |
|
---|
[3715] | 33 | import org.openstreetmap.josm.Main;
|
---|
[7425] | 34 | import org.openstreetmap.josm.data.ProjectionBounds;
|
---|
[3715] | 35 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
| 36 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[3747] | 37 | import org.openstreetmap.josm.data.imagery.GeorefImage.State;
|
---|
[3715] | 38 | import org.openstreetmap.josm.data.imagery.ImageryInfo;
|
---|
| 39 | import org.openstreetmap.josm.gui.MapView;
|
---|
| 40 | import org.openstreetmap.josm.gui.layer.WMSLayer;
|
---|
| 41 | import org.openstreetmap.josm.io.OsmTransferException;
|
---|
| 42 | import org.openstreetmap.josm.io.ProgressInputStream;
|
---|
[7132] | 43 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
[4065] | 44 | import org.openstreetmap.josm.tools.Utils;
|
---|
[7425] | 45 | import org.w3c.dom.Document;
|
---|
| 46 | import org.w3c.dom.NodeList;
|
---|
| 47 | import org.xml.sax.InputSource;
|
---|
| 48 | import org.xml.sax.SAXException;
|
---|
[3715] | 49 |
|
---|
[7425] | 50 | /**
|
---|
| 51 | * WMS grabber, fetching tiles from WMS server.
|
---|
| 52 | * @since 3715
|
---|
| 53 | */
|
---|
| 54 | public class WMSGrabber implements Runnable {
|
---|
[3715] | 55 |
|
---|
[7425] | 56 | protected final MapView mv;
|
---|
| 57 | protected final WMSLayer layer;
|
---|
| 58 | private final boolean localOnly;
|
---|
| 59 |
|
---|
| 60 | protected ProjectionBounds b;
|
---|
| 61 | protected volatile boolean canceled;
|
---|
| 62 |
|
---|
[3715] | 63 | protected String baseURL;
|
---|
[4432] | 64 | private ImageryInfo info;
|
---|
[7005] | 65 | private Map<String, String> props = new HashMap<>();
|
---|
[3715] | 66 |
|
---|
[7425] | 67 | /**
|
---|
| 68 | * Constructs a new {@code WMSGrabber}.
|
---|
| 69 | * @param mv Map view
|
---|
| 70 | * @param layer WMS layer
|
---|
| 71 | */
|
---|
[4745] | 72 | public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
|
---|
[7425] | 73 | this.mv = mv;
|
---|
| 74 | this.layer = layer;
|
---|
| 75 | this.localOnly = localOnly;
|
---|
[4432] | 76 | this.info = layer.getInfo();
|
---|
| 77 | this.baseURL = info.getUrl();
|
---|
[7425] | 78 | if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
|
---|
[4228] | 79 | props.put("Cookie", layer.getInfo().getCookies());
|
---|
| 80 | }
|
---|
| 81 | Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
|
---|
| 82 | StringBuffer output = new StringBuffer();
|
---|
| 83 | Matcher matcher = pattern.matcher(this.baseURL);
|
---|
| 84 | while (matcher.find()) {
|
---|
| 85 | props.put(matcher.group(1),matcher.group(2));
|
---|
| 86 | matcher.appendReplacement(output, "");
|
---|
| 87 | }
|
---|
| 88 | matcher.appendTail(output);
|
---|
| 89 | this.baseURL = output.toString();
|
---|
[3715] | 90 | }
|
---|
| 91 |
|
---|
[7425] | 92 | int width() {
|
---|
| 93 | return layer.getBaseImageWidth();
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | int height() {
|
---|
| 97 | return layer.getBaseImageHeight();
|
---|
| 98 | }
|
---|
| 99 |
|
---|
[3715] | 100 | @Override
|
---|
[7425] | 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 {
|
---|
[3715] | 163 | URL url = null;
|
---|
| 164 | try {
|
---|
| 165 | url = getURL(
|
---|
[4065] | 166 | b.minEast, b.minNorth,
|
---|
| 167 | b.maxEast, b.maxNorth,
|
---|
[3715] | 168 | width(), height());
|
---|
[7425] | 169 | request.finish(State.IMAGE, grab(request, url, attempt), null);
|
---|
[3715] | 170 |
|
---|
[7425] | 171 | } catch (IOException | OsmTransferException e) {
|
---|
[6643] | 172 | Main.error(e);
|
---|
[7425] | 173 | throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);
|
---|
[3715] | 174 | }
|
---|
| 175 | }
|
---|
| 176 |
|
---|
[7425] | 177 | public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
|
---|
[3715] | 178 |
|
---|
| 179 | protected URL getURL(double w, double s,double e,double n,
|
---|
| 180 | int wi, int ht) throws MalformedURLException {
|
---|
[4126] | 181 | String myProj = Main.getProjection().toCode();
|
---|
[5017] | 182 | if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) {
|
---|
[4126] | 183 | LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
|
---|
| 184 | LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
|
---|
[3715] | 185 | myProj = "EPSG:4326";
|
---|
| 186 | s = sw.lat();
|
---|
| 187 | w = sw.lon();
|
---|
| 188 | n = ne.lat();
|
---|
| 189 | e = ne.lon();
|
---|
| 190 | }
|
---|
[7012] | 191 | if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) {
|
---|
[4857] | 192 | myProj = "CRS:84";
|
---|
| 193 | }
|
---|
[3715] | 194 |
|
---|
[5017] | 195 | // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
|
---|
[4857] | 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.
|
---|
[6920] | 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
|
---|
[4857] | 210 | boolean switchLatLon = false;
|
---|
| 211 | if (baseURL.toLowerCase().contains("crs=epsg:4326")) {
|
---|
| 212 | switchLatLon = true;
|
---|
[7012] | 213 | } else if (baseURL.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProj)) {
|
---|
[4857] | 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 | }
|
---|
[4432] | 222 | return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj)
|
---|
[4857] | 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"));
|
---|
[3715] | 231 | }
|
---|
| 232 |
|
---|
| 233 | public boolean loadFromCache(WMSRequest request) {
|
---|
[7132] | 234 | BufferedImage cached = layer.cache.getExactMatch(
|
---|
| 235 | Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
|
---|
[4065] | 236 |
|
---|
| 237 | if (cached != null) {
|
---|
[7425] | 238 | request.finish(State.IMAGE, cached, null);
|
---|
[4065] | 239 | return true;
|
---|
| 240 | } else if (request.isAllowPartialCacheMatch()) {
|
---|
[7132] | 241 | BufferedImage partialMatch = layer.cache.getPartialMatch(
|
---|
| 242 | Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
|
---|
[4065] | 243 | if (partialMatch != null) {
|
---|
[7425] | 244 | request.finish(State.PARTLY_IN_CACHE, partialMatch, null);
|
---|
[3715] | 245 | return true;
|
---|
| 246 | }
|
---|
[4065] | 247 | }
|
---|
| 248 |
|
---|
[7425] | 249 | if ((!request.isReal() && !layer.hasAutoDownload())){
|
---|
| 250 | request.finish(State.NOT_IN_CACHE, null, null);
|
---|
[3715] | 251 | return true;
|
---|
| 252 | }
|
---|
[4065] | 253 |
|
---|
[3715] | 254 | return false;
|
---|
| 255 | }
|
---|
| 256 |
|
---|
[7425] | 257 | protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException {
|
---|
[6248] | 258 | Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
|
---|
[3715] | 259 |
|
---|
[5587] | 260 | HttpURLConnection conn = Utils.openHttpConnection(url);
|
---|
[7425] | 261 | for (Entry<String, String> e : props.entrySet()) {
|
---|
[4228] | 262 | conn.setRequestProperty(e.getKey(), e.getValue());
|
---|
[3715] | 263 | }
|
---|
[4172] | 264 | conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000);
|
---|
| 265 | conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000);
|
---|
[3715] | 266 |
|
---|
| 267 | String contentType = conn.getHeaderField("Content-Type");
|
---|
[7425] | 268 | if (conn.getResponseCode() != 200
|
---|
| 269 | || contentType != null && !contentType.startsWith("image") ) {
|
---|
| 270 | String xml = readException(conn);
|
---|
| 271 | try {
|
---|
| 272 | DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
---|
| 273 | InputSource is = new InputSource(new StringReader(xml));
|
---|
| 274 | Document doc = db.parse(is);
|
---|
| 275 | NodeList nodes = doc.getElementsByTagName("ServiceException");
|
---|
| 276 | List<String> exceptions = new ArrayList<>(nodes.getLength());
|
---|
| 277 | for (int i = 0; i < nodes.getLength(); i++) {
|
---|
| 278 | exceptions.add(nodes.item(i).getTextContent());
|
---|
| 279 | }
|
---|
| 280 | throw new WMSException(request, url, exceptions);
|
---|
| 281 | } catch (SAXException | ParserConfigurationException ex) {
|
---|
| 282 | throw new IOException(xml, ex);
|
---|
| 283 | }
|
---|
| 284 | }
|
---|
[3715] | 285 |
|
---|
[4065] | 286 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
---|
[7033] | 287 | try (InputStream is = new ProgressInputStream(conn, null)) {
|
---|
[4065] | 288 | Utils.copyStream(is, baos);
|
---|
| 289 | }
|
---|
[3715] | 290 |
|
---|
[4065] | 291 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
---|
[7132] | 292 | BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));
|
---|
[4065] | 293 | bais.reset();
|
---|
[4745] | 294 | layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
|
---|
[3715] | 295 | return img;
|
---|
| 296 | }
|
---|
| 297 |
|
---|
| 298 | protected String readException(URLConnection conn) throws IOException {
|
---|
| 299 | StringBuilder exception = new StringBuilder();
|
---|
| 300 | InputStream in = conn.getInputStream();
|
---|
[7082] | 301 | try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
---|
[3747] | 302 | String line = null;
|
---|
| 303 | while( (line = br.readLine()) != null) {
|
---|
| 304 | // filter non-ASCII characters and control characters
|
---|
| 305 | exception.append(line.replaceAll("[^\\p{Print}]", ""));
|
---|
| 306 | exception.append('\n');
|
---|
| 307 | }
|
---|
| 308 | return exception.toString();
|
---|
[3715] | 309 | }
|
---|
| 310 | }
|
---|
| 311 | }
|
---|