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}