001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.imagery;
003
004import java.awt.HeadlessException;
005import java.io.BufferedReader;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.StringReader;
009import java.net.MalformedURLException;
010import java.net.URL;
011import java.net.URLConnection;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.HashSet;
015import java.util.List;
016import java.util.Set;
017import java.util.regex.Pattern;
018
019import javax.xml.parsers.DocumentBuilder;
020import javax.xml.parsers.DocumentBuilderFactory;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.data.Bounds;
024import org.openstreetmap.josm.data.imagery.ImageryInfo;
025import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice;
026import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
027import org.openstreetmap.josm.io.UTFInputStreamReader;
028import org.openstreetmap.josm.tools.Utils;
029import org.w3c.dom.Document;
030import org.w3c.dom.Element;
031import org.w3c.dom.Node;
032import org.w3c.dom.NodeList;
033import org.xml.sax.EntityResolver;
034import org.xml.sax.InputSource;
035import org.xml.sax.SAXException;
036
037public class WMSImagery {
038
039    public static class WMSGetCapabilitiesException extends Exception {
040        private final String incomingData;
041
042        public WMSGetCapabilitiesException(Throwable cause, String incomingData) {
043            super(cause);
044            this.incomingData = incomingData;
045        }
046
047        public String getIncomingData() {
048            return incomingData;
049        }
050    }
051
052    private List<LayerDetails> layers;
053    private URL serviceUrl;
054    private List<String> formats;
055
056    public List<LayerDetails> getLayers() {
057        return layers;
058    }
059
060    public URL getServiceUrl() {
061        return serviceUrl;
062    }
063
064    public List<String> getFormats() {
065        return formats;
066    }
067
068    String buildRootUrl() {
069        if (serviceUrl == null) {
070            return null;
071        }
072        StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
073        a.append("://");
074        a.append(serviceUrl.getHost());
075        if (serviceUrl.getPort() != -1) {
076            a.append(":");
077            a.append(serviceUrl.getPort());
078        }
079        a.append(serviceUrl.getPath());
080        a.append("?");
081        if (serviceUrl.getQuery() != null) {
082            a.append(serviceUrl.getQuery());
083            if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) {
084                a.append("&");
085            }
086        }
087        return a.toString();
088    }
089
090    public String buildGetMapUrl(Collection<LayerDetails> selectedLayers) {
091        return buildGetMapUrl(selectedLayers, "image/jpeg");
092    }
093
094    public String buildGetMapUrl(Collection<LayerDetails> selectedLayers, String format) {
095        return buildRootUrl()
096                + "FORMAT=" + format + "&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&LAYERS="
097                + Utils.join(",", Utils.transform(selectedLayers, new Utils.Function<LayerDetails, String>() {
098            @Override
099            public String apply(LayerDetails x) {
100                return x.ident;
101            }
102        }))
103                + "&STYLES=&SRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
104    }
105
106    public void attemptGetCapabilities(String serviceUrlStr) throws MalformedURLException, IOException, WMSGetCapabilitiesException {
107        URL getCapabilitiesUrl = null;
108        try {
109            if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
110                // If the url doesn't already have GetCapabilities, add it in
111                getCapabilitiesUrl = new URL(serviceUrlStr);
112                final String getCapabilitiesQuery = "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities";
113                if (getCapabilitiesUrl.getQuery() == null) {
114                    getCapabilitiesUrl = new URL(serviceUrlStr + "?" + getCapabilitiesQuery);
115                } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
116                    getCapabilitiesUrl = new URL(serviceUrlStr + "&" + getCapabilitiesQuery);
117                } else {
118                    getCapabilitiesUrl = new URL(serviceUrlStr + getCapabilitiesQuery);
119                }
120            } else {
121                // Otherwise assume it's a good URL and let the subsequent error
122                // handling systems deal with problems
123                getCapabilitiesUrl = new URL(serviceUrlStr);
124            }
125            serviceUrl = new URL(serviceUrlStr);
126        } catch (HeadlessException e) {
127            return;
128        }
129
130        Main.info("GET " + getCapabilitiesUrl.toString());
131        URLConnection openConnection = Utils.openHttpConnection(getCapabilitiesUrl);
132        InputStream inputStream = openConnection.getInputStream();
133        BufferedReader br = new BufferedReader(UTFInputStreamReader.create(inputStream, "UTF-8"));
134        String line;
135        StringBuilder ba = new StringBuilder();
136        try {
137            while ((line = br.readLine()) != null) {
138                ba.append(line);
139                ba.append("\n");
140            }
141        } finally {
142            br.close();
143        }
144        String incomingData = ba.toString();
145
146        try {
147            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
148            builderFactory.setValidating(false);
149            builderFactory.setNamespaceAware(true);
150            DocumentBuilder builder = null;
151            builder = builderFactory.newDocumentBuilder();
152            builder.setEntityResolver(new EntityResolver() {
153                @Override
154                public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
155                    Main.info("Ignoring DTD " + publicId + ", " + systemId);
156                    return new InputSource(new StringReader(""));
157                }
158            });
159            Document document = null;
160            document = builder.parse(new InputSource(new StringReader(incomingData)));
161
162            // Some WMS service URLs specify a different base URL for their GetMap service
163            Element child = getChild(document.getDocumentElement(), "Capability");
164            child = getChild(child, "Request");
165            child = getChild(child, "GetMap");
166
167            formats = new ArrayList<String>(Utils.transform(getChildren(child, "Format"), new Utils.Function<Element, String>() {
168                @Override
169                public String apply(Element x) {
170                    return x.getTextContent();
171                }
172            }));
173
174            child = getChild(child, "DCPType");
175            child = getChild(child, "HTTP");
176            child = getChild(child, "Get");
177            child = getChild(child, "OnlineResource");
178            if (child != null) {
179                String baseURL = child.getAttribute("xlink:href");
180                if (baseURL != null && !baseURL.equals(serviceUrlStr)) {
181                    Main.info("GetCapabilities specifies a different service URL: " + baseURL);
182                    serviceUrl = new URL(baseURL);
183                }
184            }
185
186            Element capabilityElem = getChild(document.getDocumentElement(), "Capability");
187            List<Element> children = getChildren(capabilityElem, "Layer");
188            layers = parseLayers(children, new HashSet<String>());
189        } catch (Exception e) {
190            throw new WMSGetCapabilitiesException(e, incomingData);
191        }
192
193    }
194
195    public ImageryInfo toImageryInfo(String name, Collection<LayerDetails> selectedLayers) {
196        ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers));
197        if (selectedLayers != null) {
198            HashSet<String> proj = new HashSet<String>();
199            for (WMSImagery.LayerDetails l : selectedLayers) {
200                proj.addAll(l.getProjections());
201            }
202            i.setServerProjections(proj);
203        }
204        return i;
205    }
206
207    private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
208        List<LayerDetails> details = new ArrayList<LayerDetails>(children.size());
209        for (Element element : children) {
210            details.add(parseLayer(element, parentCrs));
211        }
212        return details;
213    }
214
215    private LayerDetails parseLayer(Element element, Set<String> parentCrs) {
216        String name = getChildContent(element, "Title", null, null);
217        String ident = getChildContent(element, "Name", null, null);
218
219        // The set of supported CRS/SRS for this layer
220        Set<String> crsList = new HashSet<String>();
221        // ...including this layer's already-parsed parent projections
222        crsList.addAll(parentCrs);
223
224        // Parse the CRS/SRS pulled out of this layer's XML element
225        // I think CRS and SRS are the same at this point
226        List<Element> crsChildren = getChildren(element, "CRS");
227        crsChildren.addAll(getChildren(element, "SRS"));
228        for (Element child : crsChildren) {
229            String crs = (String) getContent(child);
230            if (!crs.isEmpty()) {
231                String upperCase = crs.trim().toUpperCase();
232                crsList.add(upperCase);
233            }
234        }
235
236        // Check to see if any of the specified projections are supported by JOSM
237        boolean josmSupportsThisLayer = false;
238        for (String crs : crsList) {
239            josmSupportsThisLayer |= isProjSupported(crs);
240        }
241
242        Bounds bounds = null;
243        Element bboxElem = getChild(element, "EX_GeographicBoundingBox");
244        if (bboxElem != null) {
245            // Attempt to use EX_GeographicBoundingBox for bounding box
246            double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null));
247            double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null));
248            double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null));
249            double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null));
250            bounds = new Bounds(bot, left, top, right);
251        } else {
252            // If that's not available, try LatLonBoundingBox
253            bboxElem = getChild(element, "LatLonBoundingBox");
254            if (bboxElem != null) {
255                double left = Double.parseDouble(bboxElem.getAttribute("minx"));
256                double top = Double.parseDouble(bboxElem.getAttribute("maxy"));
257                double right = Double.parseDouble(bboxElem.getAttribute("maxx"));
258                double bot = Double.parseDouble(bboxElem.getAttribute("miny"));
259                bounds = new Bounds(bot, left, top, right);
260            }
261        }
262
263        List<Element> layerChildren = getChildren(element, "Layer");
264        List<LayerDetails> childLayers = parseLayers(layerChildren, crsList);
265
266        return new LayerDetails(name, ident, crsList, josmSupportsThisLayer, bounds, childLayers);
267    }
268
269    private boolean isProjSupported(String crs) {
270        for (ProjectionChoice pc : ProjectionPreference.getProjectionChoices()) {
271            if (pc.getPreferencesFromCode(crs) != null) return true;
272        }
273        return false;
274    }
275
276    private static String getChildContent(Element parent, String name, String missing, String empty) {
277        Element child = getChild(parent, name);
278        if (child == null)
279            return missing;
280        else {
281            String content = (String) getContent(child);
282            return (!content.isEmpty()) ? content : empty;
283        }
284    }
285
286    private static Object getContent(Element element) {
287        NodeList nl = element.getChildNodes();
288        StringBuilder content = new StringBuilder();
289        for (int i = 0; i < nl.getLength(); i++) {
290            Node node = nl.item(i);
291            switch (node.getNodeType()) {
292                case Node.ELEMENT_NODE:
293                    return node;
294                case Node.CDATA_SECTION_NODE:
295                case Node.TEXT_NODE:
296                    content.append(node.getNodeValue());
297                    break;
298            }
299        }
300        return content.toString().trim();
301    }
302
303    private static List<Element> getChildren(Element parent, String name) {
304        List<Element> retVal = new ArrayList<Element>();
305        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
306            if (child instanceof Element && name.equals(child.getNodeName())) {
307                retVal.add((Element) child);
308            }
309        }
310        return retVal;
311    }
312
313    private static Element getChild(Element parent, String name) {
314        if (parent == null)
315            return null;
316        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
317            if (child instanceof Element && name.equals(child.getNodeName()))
318                return (Element) child;
319        }
320        return null;
321    }
322
323    public static class LayerDetails {
324
325        public final String name;
326        public final String ident;
327        public final List<LayerDetails> children;
328        public final Bounds bounds;
329        public final Set<String> crsList;
330        public final boolean supported;
331
332        public LayerDetails(String name, String ident, Set<String> crsList,
333                            boolean supportedLayer, Bounds bounds,
334                            List<LayerDetails> childLayers) {
335            this.name = name;
336            this.ident = ident;
337            this.supported = supportedLayer;
338            this.children = childLayers;
339            this.bounds = bounds;
340            this.crsList = crsList;
341        }
342
343        public boolean isSupported() {
344            return this.supported;
345        }
346
347        public Set<String> getProjections() {
348            return crsList;
349        }
350
351        @Override
352        public String toString() {
353            if (this.name == null || this.name.isEmpty())
354                return this.ident;
355            else
356                return this.name;
357        }
358
359    }
360}