1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.io.imagery;
|
---|
3 |
|
---|
4 | import java.awt.image.BufferedImage;
|
---|
5 | import java.io.BufferedReader;
|
---|
6 | import java.io.ByteArrayInputStream;
|
---|
7 | import java.io.ByteArrayOutputStream;
|
---|
8 | import java.io.IOException;
|
---|
9 | import java.io.InputStream;
|
---|
10 | import java.io.InputStreamReader;
|
---|
11 | import java.io.StringReader;
|
---|
12 | import java.net.HttpURLConnection;
|
---|
13 | import java.net.MalformedURLException;
|
---|
14 | import java.net.URL;
|
---|
15 | import java.net.URLConnection;
|
---|
16 | import java.nio.charset.StandardCharsets;
|
---|
17 | import java.text.DecimalFormat;
|
---|
18 | import java.text.DecimalFormatSymbols;
|
---|
19 | import java.text.NumberFormat;
|
---|
20 | import java.util.ArrayList;
|
---|
21 | import java.util.HashMap;
|
---|
22 | import java.util.List;
|
---|
23 | import java.util.Locale;
|
---|
24 | import java.util.Map;
|
---|
25 | import java.util.Map.Entry;
|
---|
26 | import java.util.regex.Matcher;
|
---|
27 | import java.util.regex.Pattern;
|
---|
28 |
|
---|
29 | import javax.xml.parsers.DocumentBuilder;
|
---|
30 | import javax.xml.parsers.DocumentBuilderFactory;
|
---|
31 | import javax.xml.parsers.ParserConfigurationException;
|
---|
32 |
|
---|
33 | import org.openstreetmap.josm.Main;
|
---|
34 | import org.openstreetmap.josm.data.ProjectionBounds;
|
---|
35 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
36 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
37 | import org.openstreetmap.josm.data.imagery.GeorefImage.State;
|
---|
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;
|
---|
43 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
44 | import org.openstreetmap.josm.tools.Utils;
|
---|
45 | import org.w3c.dom.Document;
|
---|
46 | import org.w3c.dom.NodeList;
|
---|
47 | import org.xml.sax.InputSource;
|
---|
48 | import org.xml.sax.SAXException;
|
---|
49 |
|
---|
50 | /**
|
---|
51 | * WMS grabber, fetching tiles from WMS server.
|
---|
52 | * @since 3715
|
---|
53 | */
|
---|
54 | public class WMSGrabber implements Runnable {
|
---|
55 |
|
---|
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 |
|
---|
63 | protected String baseURL;
|
---|
64 | private ImageryInfo info;
|
---|
65 | private Map<String, String> props = new HashMap<>();
|
---|
66 |
|
---|
67 | /**
|
---|
68 | * Constructs a new {@code WMSGrabber}.
|
---|
69 | * @param mv Map view
|
---|
70 | * @param layer WMS layer
|
---|
71 | */
|
---|
72 | public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
|
---|
73 | this.mv = mv;
|
---|
74 | this.layer = layer;
|
---|
75 | this.localOnly = localOnly;
|
---|
76 | this.info = layer.getInfo();
|
---|
77 | this.baseURL = info.getUrl();
|
---|
78 | if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
|
---|
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();
|
---|
90 | }
|
---|
91 |
|
---|
92 | int width() {
|
---|
93 | return layer.getBaseImageWidth();
|
---|
94 | }
|
---|
95 |
|
---|
96 | int height() {
|
---|
97 | return layer.getBaseImageHeight();
|
---|
98 | }
|
---|
99 |
|
---|
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(Locale.ENGLISH).contains("crs=epsg:4326")) {
|
---|
212 | switchLatLon = true;
|
---|
213 | } else if (baseURL.toLowerCase(Locale.ENGLISH).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 | }
|
---|