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